diff --git a/CHANGELOG.md b/CHANGELOG.md
index e441eb87..e45f3d74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Upgrade gjavah [#43](https://github.com/sbt/sbt-jni/pull/43)
- Use cmake platform build tool [#40](https://github.com/sbt/sbt-jni/issues/40)
+- Rename macro project to core and simplify Scala 3 support [#52](https://github.com/sbt/sbt-jni/pull/52)
### Fixed
- javah failed with ClassCastException [#38](https://github.com/sbt/sbt-jni/issues/38)
diff --git a/README.md b/README.md
index 9713bf0f..da49f026 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,6 @@ The second point, portability, is inherent to JNI and thus unavoidable. However
| JniLoad | Makes `@nativeLoader` annotation available, that injects code to transparently load native libraries. |
| JniNative | Adds sbt wrapper tasks around native build tools to ease building and integrating native libraries. |
| JniPackage | Packages native libraries into multi-platform fat jars. No more manual library installation! |
-| JniSyntax | Adds an alternative to `@nativeLoader` annotation syntax, that requires this plugin to be a run time dependency |
All plugins are made available by adding the following to `project/plugins.sbt`:
```scala
@@ -74,7 +73,7 @@ JNIEXPORT jint JNICALL Java_org_example_Adder_plus
The header output directory can be configured
```
-target in javah :=
// defaults to target/native/include
+javah / target := // defaults to target/native/include
```
Note that native methods declared both in Scala and Java are supported. Whereas Scala uses the `@native` annotation, Java uses the
@@ -85,8 +84,6 @@ Note that native methods declared both in Scala and Java are supported. Whereas
|--------------------------------|---------------|
| automatic, for all projects | [JniLoad.scala](plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniLoad.scala) |
-**! Important**: *`@nativeLoader` annotation works with Scala 2.x only. You may want to consider the [JniSyntax](#jnisyntax) plugin usage for the Scala 3.x projects.*
-
This plugin enables loading native libraries in a safe and transparent manner to the developer (no more explicit, static `System.load("library")` calls required). It does so by providing a class annotation which injects native loading code to all its annottees. Furthermore, in case a native library is not available on the current `java.library.path`, the code injected by the annotation will fall back to loading native libraries packaged according to the rules of `JniPackage`.
#### Example use (Scala 2.x):
@@ -106,13 +103,23 @@ object Main extends App {
}
```
-Note: this plugin is just a shorthand for adding `sbt-jni-macros` (the project in `macros/`) and the scala-macros-paradise (on Scala <= 2.13) projects as provided dependencies.
+Note: this plugin is just a shorthand for adding `sbt-jni-core` (the project in `core/`) and the scala-macros-paradise (on Scala <= 2.13) projects as provided dependencies.
+
+See the [annotation's implementation](core/src/main/scala/ch/jodersky/jni/annotations.scala) for details about the injected code.
-See the [annotation's implementation](macros/src/main/scala/ch/jodersky/jni/annotations.scala) for details about the injected code.
+#### Example use (Scala 3.x / Scala 2.x):
-#### Example use (Scala 3.x):
+Scala 3 has no macro annotations support. As a solution we don't need this to be a macro function anymore. As the result, this option requires to have an explicit dependency on the [core](./core) sub project.
+
+This plugin behavior is configurable via:
```scala
+sbtJniCoreProvided := // set to true by default, and is enough make @nativeLoader annotation work
+```
+
+```scala
+// to make the code below work the core project should be included as a dependency via
+// sbtJniCoreProvided := false
import ch.jodersky.jni.syntax.NativeLoader
// By adding this annotation, there is no need to call
@@ -124,8 +131,6 @@ class Adder(val base: Int) extends NativeLoader("adder0"):
@main def main: Unit = (new Adder(0)).plus(1)
```
-Requires [JniSyntax](#JniSyntax) plugin usage.
-
### JniNative
| Enabled | Source |
|--------------------------------|---------------|
@@ -141,8 +146,8 @@ An initial, compatible build template can be obtained by running `sbt nativeInit
Source and output directories are configurable
```scala
-sourceDirectory in nativeCompile := sourceDirectory.value / "native",
-target in nativeCompile := target.value / "native" / (nativePlatform).value,
+nativeCompile / sourceDirectory := sourceDirectory.value / "native",
+nativeCompile / target := target.value / "native" / (nativePlatform).value,
```
### JniPackage
@@ -152,30 +157,6 @@ target in nativeCompile := target.value / "native" / (nativePlatform).value,
This plugin packages native libraries produced by JniNative in a way that they can be transparently loaded with JniLoad. It uses the notion of a native "platform", defined as the architecture-kernel values returned by `uname -sm`. A native binary of a given platform is assumed to be executable on any machines of the same platform.
-### JniSyntax
-| Enabled | Source |
-|--------------------------------|---------------|
-| manual | [JniSyntax.scala](plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniSyntax.scala) |
-
-Scala 3 has no macro annotations support. JniSyntax contains syntax to ease usage of the `ch.jodersky.jni.nativeLoaderMacro` in the project (see [JniLoad](#JniLoad) section for more details). This option requires to have runtime dependencies on [macros](./macros) and [core](./core) sub projects.
-
-#### Example use (Scala 2.x / 3.x):
-
-```scala
-import ch.jodersky.jni.syntax.NativeLoader
-
-// By adding this annotation, there is no need to call
-// System.load("adder0") before accessing native methods.
-class Adder(val base: Int) extends NativeLoader("adder0") {
- @native def plus(term: Int): Int // implemented in libadder0.so
-}
-
-// The application feels like a pure Scala app.
-object Main extends App {
- (new Adder(0)).plus(1)
-}
-```
-
## Canonical Use
*Keep in mind that sbt-jni is a __suite__ of plugins, there are many other use cases. This is a just a description of the most common one.*
@@ -183,11 +164,11 @@ object Main extends App {
1. Define separate sub-projects for JVM and native sources. In `myproject/build.sbt`:
```scala
- lazy val core = project in file("myproject-core"). // regular scala code with @native methods
- dependsOn(native % Runtime) // remove this if `core` is a library, leave choice to end-user
+ lazy val core = project in file("myproject-core") // regular scala code with @native methods
+ .dependsOn(native % Runtime) // remove this if `core` is a library, leave choice to end-user
- lazy val native = project in file("myproject-native"). // native code and build script
- enablePlugin(JniNative) // JniNative needs to be explicitly enabled
+ lazy val native = project in file("myproject-native") // native code and build script
+ .enablePlugin(JniNative) // JniNative needs to be explicitly enabled
```
Note that separate projects are not strictly required. They are strongly recommended nevertheless, as a portability-convenience tradeoff: programs written in a JVM language are expected to run anywhere without recompilation, but including native libraries in jars limits this portability to only platforms of the packaged libraries. Having a separate native project enables the users to easily swap out the native library with their own implementation.
@@ -218,7 +199,7 @@ object Main extends App {
## Examples
The [plugins' unit tests](plugin/src/sbt-test/sbt-jni) offer some simple examples. They can be run individually through these steps:
-1. Publish the macros library locally `sbt publishLocal`.
+1. Publish the core library locally `sbt publishLocal`.
2. Change to the test's directory and run `sbt -Dplugin.version=`.
3. Follow the instructions in the `test` file (only enter the lines that start with ">" into sbt).
@@ -229,14 +210,15 @@ Real-world use-cases of sbt-jni include:
## Requirements and Dependencies
- projects using `JniLoad` must use Scala versions 2.11, 2.12 or 2.13
+- projects using `JniLoad` with Scala 3 should use it with
- only POSIX platforms are supported (actually, any platform that has the `uname` command available)
The goal of sbt-jni is to be the least intrusive possible. No transitive dependencies are added to projects using any plugin (some dependencies are added to the `provided` configuration, however these do not affect any downstream projects).
## Building
-Both the macro library (`sbt-jni-macros`) and the sbt plugins (`sbt-jni`) are published. Cross-building happens on a per-project basis:
+Both the core (former macros) library (`sbt-jni-core`) and the sbt plugins (`sbt-jni`) are published. Cross-building happens on a per-project basis:
-- sbt-jni-macros is built against Scala 2.11, 2.12 and 2.13
+- sbt-jni-core is built against Scala 2.11, 2.12 and 2.13
- sbt-jni is built against Scala 2.12 (the Scala version that sbt 1.x uses)
The differing Scala versions make it necessary to always cross-compile and cross-publish this project, i.e. append a "+" before every task.
diff --git a/build.sbt b/build.sbt
index eb4e5f97..60e49345 100644
--- a/build.sbt
+++ b/build.sbt
@@ -19,18 +19,18 @@ ThisBuild / developers := List(
)
lazy val root = (project in file("."))
- .aggregate(macros, core, plugin)
+ .aggregate(core, plugin)
.settings(
publish := {},
publishLocal := {},
// make sbt-pgp happy
publishTo := Some(Resolver.file("Unused transient repository", target.value / "unusedrepo")),
- addCommandAlias("test-plugin", ";+macros/publishLocal;+core/publishLocal;scripted")
+ addCommandAlias("test-plugin", ";+core/publishLocal;scripted")
)
-lazy val macros = project
+lazy val core = project
.settings(
- name := "sbt-jni-macros",
+ name := "sbt-jni-core",
scalaVersion := scalaVersions.head,
crossScalaVersions := scalaVersions,
libraryDependencies ++= {
@@ -40,13 +40,12 @@ lazy val macros = project
"org.scala-lang" % "scala-compiler" % scalaVersion.value % Provided,
"org.scala-lang" % "scala-reflect" % scalaVersion.value
)
- case _ => Seq("org.scala-lang" %% "scala3-compiler" % scalaVersion.value)
+ case _ => Seq("org.scala-lang" %% "scala3-compiler" % scalaVersion.value % Provided)
}
},
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
- case Some((2, n)) if n >= 13 => Seq()
- case Some((2, n)) =>
+ case Some((2, n)) if n < 13 =>
Seq(compilerPlugin(("org.scalamacros" % "paradise" % macrosParadiseVersion).cross(CrossVersion.full)))
case _ => Seq()
}
@@ -59,14 +58,6 @@ lazy val macros = project
}
)
-lazy val core = project
- .dependsOn(macros)
- .settings(
- name := "sbt-jni-core",
- scalaVersion := scalaVersions.head,
- crossScalaVersions := scalaVersions
- )
-
lazy val plugin = project
.enablePlugins(SbtPlugin)
.settings(
@@ -80,7 +71,7 @@ lazy val plugin = project
|
|private[jni] object ProjectVersion {
| final val MacrosParadise = "${macrosParadiseVersion}"
- | final val Macros = "${version.value}"
+ | final val Core = "${version.value}"
|}
|""".stripMargin
val file = sourceManaged.value / "ch" / "jodersky" / "sbt" / "jni" / "ProjectVersion.scala"
diff --git a/core/src/main/scala-2/ch/jodersky/jni/Process.scala b/core/src/main/scala-2/ch/jodersky/jni/Process.scala
new file mode 100644
index 00000000..0b22e508
--- /dev/null
+++ b/core/src/main/scala-2/ch/jodersky/jni/Process.scala
@@ -0,0 +1,10 @@
+package ch.jodersky.jni
+
+object Process {
+ def out(command: String): String =
+ try {
+ scala.sys.process.Process("uname -sm").lineStream.head
+ } catch {
+ case ex: Exception => sys.error("Error running `uname` command")
+ }
+}
diff --git a/macros/src/main/scala-2/ch/jodersky/jni/annotations.scala b/core/src/main/scala-2/ch/jodersky/jni/annotations.scala
similarity index 100%
rename from macros/src/main/scala-2/ch/jodersky/jni/annotations.scala
rename to core/src/main/scala-2/ch/jodersky/jni/annotations.scala
diff --git a/core/src/main/scala-3/ch/jodersky/jni/Process.scala b/core/src/main/scala-3/ch/jodersky/jni/Process.scala
new file mode 100644
index 00000000..996e4c5e
--- /dev/null
+++ b/core/src/main/scala-3/ch/jodersky/jni/Process.scala
@@ -0,0 +1,10 @@
+package ch.jodersky.jni
+
+object Process {
+ def out(command: String): String =
+ try {
+ scala.sys.process.Process("uname -sm").lazyLines.head
+ } catch {
+ case ex: Exception => sys.error("Error running `uname` command")
+ }
+}
diff --git a/core/src/main/scala/ch/jodersky/jni/syntax/NativeLoader.scala b/core/src/main/scala/ch/jodersky/jni/syntax/NativeLoader.scala
index 675dad77..02fb49dc 100644
--- a/core/src/main/scala/ch/jodersky/jni/syntax/NativeLoader.scala
+++ b/core/src/main/scala/ch/jodersky/jni/syntax/NativeLoader.scala
@@ -1,7 +1,58 @@
package ch.jodersky.jni.syntax
-import ch.jodersky.jni.nativeLoaderMacro
+import ch.jodersky.jni.Process
+
+import java.nio.file.{Files, Path}
class NativeLoader(nativeLibrary: String) {
- nativeLoaderMacro.load(nativeLibrary)
+ NativeLoader.load(nativeLibrary)
+}
+
+object NativeLoader {
+ def load(nativeLibrary: String): Unit = {
+ def loadPackaged(): Unit = {
+
+ val lib: String = System.mapLibraryName(nativeLibrary)
+
+ val tmp: Path = Files.createTempDirectory("jni-")
+ val plat: String = {
+ val line = Process.out("uname -sm")
+ val parts = line.split(" ")
+ if (parts.length != 2) {
+ sys.error("Could not determine platform: 'uname -sm' returned unexpected string: " + line)
+ } else {
+ val arch = parts(1).toLowerCase.replaceAll("\\s", "")
+ val kernel = parts(0).toLowerCase.replaceAll("\\s", "")
+ arch + "-" + kernel
+ }
+ }
+
+ val resourcePath: String = "/native/" + plat + "/" + lib
+ val resourceStream = Option(this.getClass.getResourceAsStream(resourcePath)) match {
+ case Some(s) => s
+ case None =>
+ throw new UnsatisfiedLinkError(
+ "Native library " + lib + " (" + resourcePath + ") cannot be found on the classpath."
+ )
+ }
+
+ val extractedPath = tmp.resolve(lib)
+
+ try {
+ Files.copy(resourceStream, extractedPath)
+ } catch {
+ case ex: Exception => throw new UnsatisfiedLinkError("Error while extracting native library: " + ex)
+ }
+
+ System.load(extractedPath.toAbsolutePath.toString)
+ }
+
+ def load(): Unit = try {
+ System.loadLibrary(nativeLibrary)
+ } catch {
+ case ex: UnsatisfiedLinkError => loadPackaged()
+ }
+
+ load()
+ }
}
diff --git a/macros/src/main/scala-2/ch/jodersky/jni/nativeLoaderMacro.scala b/macros/src/main/scala-2/ch/jodersky/jni/nativeLoaderMacro.scala
deleted file mode 100644
index e14c2cfc..00000000
--- a/macros/src/main/scala-2/ch/jodersky/jni/nativeLoaderMacro.scala
+++ /dev/null
@@ -1,62 +0,0 @@
-package ch.jodersky.jni
-
-import scala.reflect.macros.whitebox.Context
-import scala.language.experimental.macros
-
-object nativeLoaderMacro {
- def load(nativeLibrary: String): Unit = macro nativeLoaderMacro.impl
-
- def impl(c: Context)(nativeLibrary: c.Expr[String]): c.Expr[Unit] = {
- import c.universe._
- c.Expr(q"""
- {
- def loadPackaged(): Unit = {
- import ch.jodersky.jni.nativeLoaderMacro
- import java.nio.file.{Files, Path}
-
- val lib: String = System.mapLibraryName($nativeLibrary)
-
- val tmp: Path = Files.createTempDirectory("jni-")
- val plat: String = {
- val line = try {
- scala.sys.process.Process("uname -sm").lineStream.head
- } catch {
- case ex: Exception => sys.error("Error running `uname` command")
- }
- val parts = line.split(" ")
- if (parts.length != 2) {
- sys.error("Could not determine platform: 'uname -sm' returned unexpected string: " + line)
- } else {
- val arch = parts(1).toLowerCase.replaceAll("\\s", "")
- val kernel = parts(0).toLowerCase.replaceAll("\\s", "")
- arch + "-" + kernel
- }
- }
-
- val resourcePath: String = "/native/" + plat + "/" + lib
- val resourceStream = Option(nativeLoaderMacro.getClass.getResourceAsStream(resourcePath)) match {
- case Some(s) => s
- case None => throw new UnsatisfiedLinkError("Native library " + lib + " (" + resourcePath + ") cannot be found on the classpath.")
- }
-
- val extractedPath = tmp.resolve(lib)
-
- try {
- Files.copy(resourceStream, extractedPath)
- } catch {
- case ex: Exception => throw new UnsatisfiedLinkError("Error while extracting native library: " + ex)
- }
-
- System.load(extractedPath.toAbsolutePath.toString)
- }
-
- def load(): Unit = try {
- System.loadLibrary($nativeLibrary)
- } catch {
- case ex: UnsatisfiedLinkError => loadPackaged()
- }
-
- load()
- }""")
- }
-}
diff --git a/macros/src/main/scala-3/ch/jodersky/jni/nativeLoaderMacro.scala b/macros/src/main/scala-3/ch/jodersky/jni/nativeLoaderMacro.scala
deleted file mode 100644
index cbb03e42..00000000
--- a/macros/src/main/scala-3/ch/jodersky/jni/nativeLoaderMacro.scala
+++ /dev/null
@@ -1,58 +0,0 @@
-package ch.jodersky.jni
-
-import quoted.*
-
-object nativeLoaderMacro:
- inline def load(nativeLibrary: String) = ${ nativeLoaderMacro.impl('nativeLibrary) }
-
- def impl(nativeLibrary: Expr[String])(using qctx: Quotes): Expr[Unit] =
- '{
- def loadPackaged(): Unit = {
- import ch.jodersky.jni.nativeLoaderMacro
- import java.nio.file.{Files, Path}
-
- val lib: String = System.mapLibraryName($nativeLibrary)
-
- val tmp: Path = Files.createTempDirectory("jni-")
- val plat: String = {
- val line =
- try {
- scala.sys.process.Process("uname -sm").lazyLines.head
- } catch {
- case ex: Exception => sys.error("Error running `uname` command")
- }
- val parts = line.split(" ")
- if (parts.length != 2) {
- sys.error("Could not determine platform: 'uname -sm' returned unexpected string: " + line)
- } else {
- val arch = parts(1).toLowerCase.replaceAll("\\s", "")
- val kernel = parts(0).toLowerCase.replaceAll("\\s", "")
- arch + "-" + kernel
- }
- }
- val resourcePath: String = "/native/" + plat + "/" + lib
- val resourceStream = Option(nativeLoaderMacro.getClass.getResourceAsStream(resourcePath)) match {
- case Some(s) => s
- case None =>
- throw new UnsatisfiedLinkError(
- "Native library " + lib + " (" + resourcePath + ") cannot be found on the classpath."
- )
- }
-
- val extractedPath = tmp.resolve(lib)
-
- try {
- Files.copy(resourceStream, extractedPath)
- } catch {
- case ex: Exception => throw new UnsatisfiedLinkError("Error while extracting native library: " + ex)
- }
-
- System.load(extractedPath.toAbsolutePath.toString)
- }
- def load(): Unit = try {
- System.loadLibrary($nativeLibrary)
- } catch {
- case ex: UnsatisfiedLinkError => loadPackaged()
- }
- load()
- }
diff --git a/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniLoad.scala b/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniLoad.scala
index 379d3475..3f6b3c93 100644
--- a/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniLoad.scala
+++ b/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniLoad.scala
@@ -9,7 +9,19 @@ object JniLoad extends AutoPlugin {
override def requires = empty
override def trigger = allRequirements
+ object autoImport {
+
+ val sbtJniCoreProvided = settingKey[Boolean](
+ "Determines if macro dependecy is Provided. The default value is true." +
+ "if set to false the macro would be a runtime dependency (required for Scala 3.x)."
+ )
+
+ }
+
+ import autoImport._
+
lazy val settings: Seq[Setting[_]] = Seq(
+ sbtJniCoreProvided := true,
// Macro Paradise plugin and dependencies are needed to expand annotation macros.
// Once expanded however, downstream projects don't need these dependencies anymore
// (hence the "Provided" configuration).
@@ -28,12 +40,16 @@ object JniLoad extends AutoPlugin {
}
},
libraryDependencies ++= {
+ val scope = if (sbtJniCoreProvided.value) Provided else Compile
CrossVersion.partialVersion(scalaVersion.value) match {
- case Some((2, n)) => Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided)
+ case Some((2, n)) => Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value % scope)
case _ => Seq()
}
},
- libraryDependencies += "ch.jodersky" %% "sbt-jni-macros" % ProjectVersion.Macros % Provided,
+ libraryDependencies += {
+ val scope = if (sbtJniCoreProvided.value) Provided else Compile
+ "ch.jodersky" %% "sbt-jni-core" % ProjectVersion.Core % scope
+ },
resolvers += Resolver.jcenterRepo
)
diff --git a/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniSyntax.scala b/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniSyntax.scala
deleted file mode 100644
index 2ce96909..00000000
--- a/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniSyntax.scala
+++ /dev/null
@@ -1,39 +0,0 @@
-package ch.jodersky.sbt.jni
-package plugins
-
-import sbt._
-import sbt.Keys._
-
-object JniSyntax extends AutoPlugin {
-
- lazy val settings: Seq[Setting[_]] = Seq(
- // Macro Paradise plugin and dependencies are needed to expand annotation macros.
- // Once expanded however, downstream projects don't need these dependencies anymore
- // (hence the "Provided" configuration).
- libraryDependencies ++= {
- CrossVersion.partialVersion(scalaVersion.value) match {
- case Some((2, n)) if n >= 13 => Seq()
- case Some((2, n)) =>
- Seq(compilerPlugin(("org.scalamacros" % "paradise" % ProjectVersion.MacrosParadise).cross(CrossVersion.full)))
- case _ => Seq()
- }
- },
- Compile / scalacOptions ++= {
- CrossVersion.partialVersion(scalaVersion.value) match {
- case Some((2, n)) if n >= 13 => Seq("-Ymacro-annotations")
- case _ => Seq()
- }
- },
- libraryDependencies ++= {
- CrossVersion.partialVersion(scalaVersion.value) match {
- case Some((2, n)) => Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value)
- case _ => Seq()
- }
- },
- libraryDependencies += "ch.jodersky" %% "sbt-jni-core" % ProjectVersion.Macros,
- resolvers += Resolver.jcenterRepo
- )
-
- override def projectSettings = settings
-
-}
diff --git a/plugin/src/sbt-test/sbt-jni/simple-syntax/build.sbt b/plugin/src/sbt-test/sbt-jni/simple-syntax/build.sbt
index 89c1012f..29dc5e49 100644
--- a/plugin/src/sbt-test/sbt-jni/simple-syntax/build.sbt
+++ b/plugin/src/sbt-test/sbt-jni/simple-syntax/build.sbt
@@ -5,8 +5,8 @@ lazy val root = (project in file(".")).aggregate(core, native)
lazy val core = project
.settings(libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test)
.settings(javah / target := (native / nativeCompile / sourceDirectory).value / "include")
+ .settings(sbtJniCoreProvided := false)
.dependsOn(native % Runtime)
- .enablePlugins(JniSyntax)
lazy val native = project
.settings(nativeCompile / sourceDirectory := sourceDirectory.value)