From a3641ae433b00535e9e8d56ded41659733af9633 Mon Sep 17 00:00:00 2001 From: thirstycrow <5451vs5451@gmail.com> Date: Sat, 5 Oct 2019 09:12:46 +0800 Subject: [PATCH 1/3] Build JavaCPP libraries at compile time --- README.markdown | 4 +- .../{Plugin.scala => JavaCppPlugin.scala} | 46 ++++++++++++---- src/sbt-test/sbt-javacpp/simple/build.sbt | 4 +- .../sbt-javacpp/simple/project/plugins.sbt | 2 + .../java/javacpp/PointerVectorVector.java | 31 +++++++++++ .../simple/src/main/scala/Main.scala | 3 -- .../src/test/java/javacpp/VectorTest.java | 54 ------------------- .../src/test/scala/javacpp/VectorTest.scala | 17 ++++++ 8 files changed, 91 insertions(+), 70 deletions(-) rename src/main/scala/org/bytedeco/sbt/javacpp/{Plugin.scala => JavaCppPlugin.scala} (59%) create mode 100644 src/sbt-test/sbt-javacpp/simple/src/main/java/javacpp/PointerVectorVector.java delete mode 100644 src/sbt-test/sbt-javacpp/simple/src/main/scala/Main.scala delete mode 100644 src/sbt-test/sbt-javacpp/simple/src/test/java/javacpp/VectorTest.java create mode 100644 src/sbt-test/sbt-javacpp/simple/src/test/scala/javacpp/VectorTest.scala diff --git a/README.markdown b/README.markdown index ef6a175..6599bde 100644 --- a/README.markdown +++ b/README.markdown @@ -42,8 +42,8 @@ you can change the target platform for your build straight from your command lin sbt compile -Dsbt.javacpp.platform="android-arm android-x86" ``` -In case you want to select a different javacpp version: +In case you want to select a different javacpp version, add the following line in your `project/plugins.sbt`: ```scala -javaCppVersion := "1.4.3" +libraryDependencies += "org.bytedeco" % "javacpp" % "1.5.1" ``` diff --git a/src/main/scala/org/bytedeco/sbt/javacpp/Plugin.scala b/src/main/scala/org/bytedeco/sbt/javacpp/JavaCppPlugin.scala similarity index 59% rename from src/main/scala/org/bytedeco/sbt/javacpp/Plugin.scala rename to src/main/scala/org/bytedeco/sbt/javacpp/JavaCppPlugin.scala index edf80df..fa790c7 100644 --- a/src/main/scala/org/bytedeco/sbt/javacpp/Plugin.scala +++ b/src/main/scala/org/bytedeco/sbt/javacpp/JavaCppPlugin.scala @@ -1,34 +1,40 @@ package org.bytedeco.sbt.javacpp -import scala.language.postfixOps -import sbt._ +import org.bytedeco.javacpp.tools.Builder import sbt.Keys._ +import sbt._ +import scala.language.postfixOps import scala.util.Try -object Plugin extends AutoPlugin { +object JavaCppPlugin extends AutoPlugin { override def projectSettings: Seq[Setting[_]] = { import autoImport._ Seq( autoCompilerPlugins := true, javaCppPlatform := Platform.current, - javaCppVersion := Versions.javaCppVersion, javaCppPresetLibs := Seq.empty, libraryDependencies += { - "org.bytedeco" % "javacpp" % javaCppVersion.value jar + "org.bytedeco" % "javacpp" % Versions.javaCppVersion jar }, - javaCppPresetDependencies) + javaCppPresetDependencies, + javaCppBuild := javaCpp.value, + products in Compile := (products in Compile).dependsOn(javaCppBuild).value) } object Versions { - val javaCppVersion = "1.4.3" + val javaCppVersion = { + val javaCppJar = classOf[Builder].getProtectionDomain.getCodeSource.getLocation.getFile + "(?<=javacpp-)(.*)(?=\\.jar)".r.findFirstIn(javaCppJar).get + } } object autoImport { val javaCppPlatform = SettingKey[Seq[String]]("javaCppPlatform", """The platform that you want to compile for (defaults to the platform of the current computer). You can also set this via the "sbt.javacpp.platform" System Property """) - val javaCppVersion = SettingKey[String]("javaCppVersion", s"Version of Java CPP that you want to use, defaults to ${Versions.javaCppVersion}") val javaCppPresetLibs = SettingKey[Seq[(String, String)]]("javaCppPresetLibs", "List of additional JavaCPP presets that you would wish to bind lazily, defaults to an empty list") + val javaCppClasses = SettingKey[Seq[String]]("javaCppClasses", "A list of Java CPP classes. Suffix of '.*' ('.**') can be used to match all classes under the specified package (and any subpackages)") + val javaCppBuild = TaskKey[Seq[File]]("javaCppBuild", "Build Java CPP products") } override def requires: Plugins = plugins.JvmPlugin @@ -38,7 +44,7 @@ object Plugin extends AutoPlugin { private def javaCppPresetDependencies: Def.Setting[Seq[ModuleID]] = { import autoImport._ libraryDependencies ++= { - val cppPresetVersion = buildPresetVersion(javaCppVersion.value) + lazy val cppPresetVersion = buildPresetVersion(Versions.javaCppVersion) javaCppPresetLibs.value.flatMap { case (libName, libVersion) => val generic = "org.bytedeco.javacpp-presets" % libName % s"$libVersion-$cppPresetVersion" classifier "" @@ -71,4 +77,24 @@ object Plugin extends AutoPlugin { Try(arg.split('.').map(_.toInt).toList).toOption } -} \ No newline at end of file + private def javaCpp = Def.task { + import autoImport._ + val classes = javaCppClasses.value + val dependencies = (dependencyClasspath in Compile).value + val output = (classDirectory in Compile).value + val _ = (compile in Compile).value + + val thread = Thread.currentThread() + val saved = thread.getContextClassLoader + thread.setContextClassLoader(classOf[Builder].getClassLoader) + try { + new Builder() + .classPaths(output.getAbsolutePath) + .classPaths(dependencies.map(_.data.absolutePath): _*) + .classesOrPackages(classes: _*) + .build() + } finally { + thread.setContextClassLoader(saved) + } + } +} diff --git a/src/sbt-test/sbt-javacpp/simple/build.sbt b/src/sbt-test/sbt-javacpp/simple/build.sbt index c37a302..df26c1c 100644 --- a/src/sbt-test/sbt-javacpp/simple/build.sbt +++ b/src/sbt-test/sbt-javacpp/simple/build.sbt @@ -2,4 +2,6 @@ version := "0.1" scalaVersion := "2.11.12" -libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test" \ No newline at end of file +javaCppClasses := Seq("javacpp.*") + +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % Test diff --git a/src/sbt-test/sbt-javacpp/simple/project/plugins.sbt b/src/sbt-test/sbt-javacpp/simple/project/plugins.sbt index fba3ef8..d82a7fe 100644 --- a/src/sbt-test/sbt-javacpp/simple/project/plugins.sbt +++ b/src/sbt-test/sbt-javacpp/simple/project/plugins.sbt @@ -5,3 +5,5 @@ |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else addSbtPlugin("org.bytedeco" % "sbt-javacpp" % pluginVersion) } + +libraryDependencies += "org.bytedeco" % "javacpp" % "1.5.1" diff --git a/src/sbt-test/sbt-javacpp/simple/src/main/java/javacpp/PointerVectorVector.java b/src/sbt-test/sbt-javacpp/simple/src/main/java/javacpp/PointerVectorVector.java new file mode 100644 index 0000000..82c1f8a --- /dev/null +++ b/src/sbt-test/sbt-javacpp/simple/src/main/java/javacpp/PointerVectorVector.java @@ -0,0 +1,31 @@ +package javacpp; + +import org.bytedeco.javacpp.*; +import org.bytedeco.javacpp.annotation.*; + +@Platform(include="") +@Name("std::vector >") +public class PointerVectorVector extends Pointer { + static { Loader.load(); } + public PointerVectorVector() { allocate(); } + public PointerVectorVector(long n) { allocate(n); } + public PointerVectorVector(Pointer p) { super(p); } // this = (vector >*)p + private native void allocate(); // this = new vector >() + private native void allocate(long n); // this = new vector >(n) + @Name("operator=") + public native @ByRef PointerVectorVector put(@ByRef PointerVectorVector x); + + @Name("operator[]") + public native @StdVector PointerPointer get(long n); + public native @StdVector PointerPointer at(long n); + + public native long size(); + public native @Cast("bool") boolean empty(); + public native void resize(long n); + public native @Index long size(long i); // return (*this)[i].size() + public native @Index @Cast("bool") boolean empty(long i); // return (*this)[i].empty() + public native @Index void resize(long i, long n); // (*this)[i].resize(n) + + public native @Index Pointer get(long i, long j); // return (*this)[i][j] + public native void put(long i, long j, Pointer p); // (*this)[i][j] = p +} diff --git a/src/sbt-test/sbt-javacpp/simple/src/main/scala/Main.scala b/src/sbt-test/sbt-javacpp/simple/src/main/scala/Main.scala deleted file mode 100644 index be54157..0000000 --- a/src/sbt-test/sbt-javacpp/simple/src/main/scala/Main.scala +++ /dev/null @@ -1,3 +0,0 @@ -object Main extends App { - println("hello") -} \ No newline at end of file diff --git a/src/sbt-test/sbt-javacpp/simple/src/test/java/javacpp/VectorTest.java b/src/sbt-test/sbt-javacpp/simple/src/test/java/javacpp/VectorTest.java deleted file mode 100644 index aec1635..0000000 --- a/src/sbt-test/sbt-javacpp/simple/src/test/java/javacpp/VectorTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package javacpp; - -import java.io.File; -import org.bytedeco.javacpp.*; -import org.bytedeco.javacpp.annotation.*; -import org.bytedeco.javacpp.tools.Builder; -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.BeforeClass; - -@Platform(include="") -public class VectorTest { - - @BeforeClass public static void setUpClass() throws Exception { - Class c = VectorTest.class; - Builder builder = new Builder().classesOrPackages(c.getName()); - File[] outputFiles = builder.build(); - Loader.load(c); - } - - @Name("std::vector >") - public static class PointerVectorVector extends Pointer { - static { Loader.load(); } - public PointerVectorVector() { allocate(); } - public PointerVectorVector(long n) { allocate(n); } - public PointerVectorVector(Pointer p) { super(p); } // this = (vector >*)p - private native void allocate(); // this = new vector >() - private native void allocate(long n); // this = new vector >(n) - @Name("operator=") - public native @ByRef PointerVectorVector put(@ByRef PointerVectorVector x); - - @Name("operator[]") - public native @StdVector PointerPointer get(long n); - public native @StdVector PointerPointer at(long n); - - public native long size(); - public native @Cast("bool") boolean empty(); - public native void resize(long n); - public native @Index long size(long i); // return (*this)[i].size() - public native @Index @Cast("bool") boolean empty(long i); // return (*this)[i].empty() - public native @Index void resize(long i, long n); // (*this)[i].resize(n) - - public native @Index Pointer get(long i, long j); // return (*this)[i][j] - public native void put(long i, long j, Pointer p); // (*this)[i][j] = p - } - - @Test public void testPointerVectorVector() { - PointerVectorVector v = new PointerVectorVector(13); - v.resize(0, 42); // v[0].resize(42) - Pointer p = new Pointer() { { address = 0xDEADBEEFL; } }; - v.put(0, 0, p); // v[0][0] = p - assertTrue(!v.empty()); - } -} \ No newline at end of file diff --git a/src/sbt-test/sbt-javacpp/simple/src/test/scala/javacpp/VectorTest.scala b/src/sbt-test/sbt-javacpp/simple/src/test/scala/javacpp/VectorTest.scala new file mode 100644 index 0000000..abfb77a --- /dev/null +++ b/src/sbt-test/sbt-javacpp/simple/src/test/scala/javacpp/VectorTest.scala @@ -0,0 +1,17 @@ +package javacpp; + +import org.scalatest._ +import org.bytedeco.javacpp._ + +class VectorTest extends FlatSpec with BeforeAndAfterAll { + + it should "test PointerVectorVector" in { + val v = new PointerVectorVector(13) + v.resize(0, 42) // v[0].resize(42) + val p = new Pointer() { + address = 0xDEADBEEFL + } + v.put(0, 0, p) // v[0][0] = p + assert(!v.empty) + } +} From 01bdeb0b027e50b4bd45cd1ac76691d31bce92ca Mon Sep 17 00:00:00 2001 From: thirstycrow <5451vs5451@gmail.com> Date: Sun, 6 Oct 2019 22:41:23 +0800 Subject: [PATCH 2/3] Fix error installing oraclejdk8: https://travis-ci.community/t/error-installing-oraclejdk8-expected-feature-release-number-in-range-of-9-to-14-but-got-8/3766 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d5ee2f6..11c7e3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: trusty sudo: false language: scala jdk: From 0298ecd6fc394b8727ae81feef0a46e762281adb Mon Sep 17 00:00:00 2001 From: thirstycrow <5451vs5451@gmail.com> Date: Fri, 18 Oct 2019 12:52:08 +0800 Subject: [PATCH 3/3] Implement the build task using reflection to remove javacpp library as a dependency of the plugin --- README.markdown | 4 +- build.sbt | 2 - .../bytedeco/sbt/javacpp/JavaCppPlugin.scala | 72 ++++++++++++++----- .../org/bytedeco/sbt/javacpp/Platform.scala | 30 ++++++-- src/sbt-test/sbt-javacpp/simple/build.sbt | 4 ++ .../sbt-javacpp/simple/project/plugins.sbt | 2 - 6 files changed, 86 insertions(+), 28 deletions(-) diff --git a/README.markdown b/README.markdown index 6599bde..ef6a175 100644 --- a/README.markdown +++ b/README.markdown @@ -42,8 +42,8 @@ you can change the target platform for your build straight from your command lin sbt compile -Dsbt.javacpp.platform="android-arm android-x86" ``` -In case you want to select a different javacpp version, add the following line in your `project/plugins.sbt`: +In case you want to select a different javacpp version: ```scala -libraryDependencies += "org.bytedeco" % "javacpp" % "1.5.1" +javaCppVersion := "1.4.3" ``` diff --git a/build.sbt b/build.sbt index 21e13a6..04c96cc 100644 --- a/build.sbt +++ b/build.sbt @@ -23,8 +23,6 @@ publishArtifact in Test := false pomIncludeRepository := { _ => false } -libraryDependencies += "org.bytedeco" % "javacpp" % "1.4.3" - scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature", "-Xlint", "-Xlog-free-terms") publishTo := { diff --git a/src/main/scala/org/bytedeco/sbt/javacpp/JavaCppPlugin.scala b/src/main/scala/org/bytedeco/sbt/javacpp/JavaCppPlugin.scala index fa790c7..af15040 100644 --- a/src/main/scala/org/bytedeco/sbt/javacpp/JavaCppPlugin.scala +++ b/src/main/scala/org/bytedeco/sbt/javacpp/JavaCppPlugin.scala @@ -1,10 +1,12 @@ package org.bytedeco.sbt.javacpp -import org.bytedeco.javacpp.tools.Builder +import java.net.URLClassLoader +import java.util.Properties + import sbt.Keys._ import sbt._ -import scala.language.postfixOps +import scala.language.{ postfixOps, reflectiveCalls } import scala.util.Try object JavaCppPlugin extends AutoPlugin { @@ -13,10 +15,13 @@ object JavaCppPlugin extends AutoPlugin { import autoImport._ Seq( autoCompilerPlugins := true, + javaCppClasses := Seq.empty, + javaCppCustomizer := identity, javaCppPlatform := Platform.current, javaCppPresetLibs := Seq.empty, + javaCppVersion := Versions.javaCppVersion, libraryDependencies += { - "org.bytedeco" % "javacpp" % Versions.javaCppVersion jar + "org.bytedeco" % "javacpp" % javaCppVersion.value jar }, javaCppPresetDependencies, javaCppBuild := javaCpp.value, @@ -24,17 +29,48 @@ object JavaCppPlugin extends AutoPlugin { } object Versions { - val javaCppVersion = { - val javaCppJar = classOf[Builder].getProtectionDomain.getCodeSource.getLocation.getFile - "(?<=javacpp-)(.*)(?=\\.jar)".r.findFirstIn(javaCppJar).get - } + val javaCppVersion = "1.4.3" } object autoImport { + type JavaCppBuilder = { + def classPaths(classPath: String): this.type + def classPaths(classPath: Array[String]): this.type + def encoding(encoding: String): this.type + def outputDirectory(outputDirectory: String): this.type + def outputDirectory(outputDirectory: File): this.type + def clean(clean: Boolean): this.type + def generate(generate: Boolean): this.type + def compile(compile: Boolean): this.type + def deleteJniFiles(deleteJniFiles: Boolean): this.type + def header(header: Boolean): this.type + def copyLibs(copyLibs: Boolean): this.type + def copyResources(copyResources: Boolean): this.type + def outputName(outputName: String): this.type + def jarPrefix(jarPrefix: String): this.type + def properties(platform: String): this.type + def properties(properties: Properties): this.type + def propertyFile(propertyFile: String): this.type + def propertyFile(propertyFile: File): this.type + def property(keyValue: String): this.type + def property(key: String, value: String): this.type + def classesOrPackages(classes: Array[String]): this.type + def buildCommand(buildCommand: Array[String]): this.type + def workingDirectory(workingDirectory: String): this.type + def workingDirectory(workingDirectory: File): this.type + def environmentVariables(environmentVariables: java.util.Map[String, String]): this.type + def compilerOptions(options: Array[String]): this.type + def build(): Array[File] + def printHelp(): Unit + def getClass(): Class[_] + } + + val javaCppBuild = TaskKey[Seq[File]]("javaCppBuild", "Build Java CPP products") + val javaCppClasses = SettingKey[Seq[String]]("javaCppClasses", "A list of Java CPP classes. Suffix of '.*' ('.**') can be used to match all classes under the specified package (and any subpackages)") + val javaCppCustomizer = SettingKey[JavaCppBuilder => JavaCppBuilder]("javaCppCustomization", "Customize the Java CPP builder") val javaCppPlatform = SettingKey[Seq[String]]("javaCppPlatform", """The platform that you want to compile for (defaults to the platform of the current computer). You can also set this via the "sbt.javacpp.platform" System Property """) val javaCppPresetLibs = SettingKey[Seq[(String, String)]]("javaCppPresetLibs", "List of additional JavaCPP presets that you would wish to bind lazily, defaults to an empty list") - val javaCppClasses = SettingKey[Seq[String]]("javaCppClasses", "A list of Java CPP classes. Suffix of '.*' ('.**') can be used to match all classes under the specified package (and any subpackages)") - val javaCppBuild = TaskKey[Seq[File]]("javaCppBuild", "Build Java CPP products") + val javaCppVersion = SettingKey[String]("javaCppVersion", s"Version of Java CPP that you want to use, defaults to ${Versions.javaCppVersion}") } override def requires: Plugins = plugins.JvmPlugin @@ -44,7 +80,7 @@ object JavaCppPlugin extends AutoPlugin { private def javaCppPresetDependencies: Def.Setting[Seq[ModuleID]] = { import autoImport._ libraryDependencies ++= { - lazy val cppPresetVersion = buildPresetVersion(Versions.javaCppVersion) + lazy val cppPresetVersion = buildPresetVersion(javaCppVersion.value) javaCppPresetLibs.value.flatMap { case (libName, libVersion) => val generic = "org.bytedeco.javacpp-presets" % libName % s"$libVersion-$cppPresetVersion" classifier "" @@ -80,19 +116,23 @@ object JavaCppPlugin extends AutoPlugin { private def javaCpp = Def.task { import autoImport._ val classes = javaCppClasses.value + val customizer = javaCppCustomizer.value val dependencies = (dependencyClasspath in Compile).value val output = (classDirectory in Compile).value val _ = (compile in Compile).value + val cl = new URLClassLoader(dependencies.map(_.data.toURI.toURL).toArray, null) val thread = Thread.currentThread() + thread.setContextClassLoader(cl) + val builder = cl.loadClass("org.bytedeco.javacpp.tools.Builder").newInstance().asInstanceOf[JavaCppBuilder] val saved = thread.getContextClassLoader - thread.setContextClassLoader(classOf[Builder].getClassLoader) + try { - new Builder() - .classPaths(output.getAbsolutePath) - .classPaths(dependencies.map(_.data.absolutePath): _*) - .classesOrPackages(classes: _*) - .build() + customizer( + builder + .classPaths(output.getAbsolutePath) + .classPaths(dependencies.map(_.data.absolutePath).toArray) + .classesOrPackages(classes.toArray)).build() } finally { thread.setContextClassLoader(saved) } diff --git a/src/main/scala/org/bytedeco/sbt/javacpp/Platform.scala b/src/main/scala/org/bytedeco/sbt/javacpp/Platform.scala index bfbd0a0..3536421 100644 --- a/src/main/scala/org/bytedeco/sbt/javacpp/Platform.scala +++ b/src/main/scala/org/bytedeco/sbt/javacpp/Platform.scala @@ -1,11 +1,8 @@ package org.bytedeco.sbt.javacpp -import org.bytedeco.javacpp.Loader - /** * Created by Lloyd on 2/22/16. */ - object Platform { private val platformOverridePropertyKey: String = "sbt.javacpp.platform" @@ -20,7 +17,28 @@ object Platform { */ val current: Seq[String] = sys.props.get(platformOverridePropertyKey) match { case Some(platform) if platform.trim().nonEmpty => platform.split(' ') - case _ => Seq(Loader.getPlatform) + case _ => + val jvmName = System.getProperty("java.vm.name", "").toLowerCase + var osName = System.getProperty("os.name", "").toLowerCase + var osArch = System.getProperty("os.arch", "").toLowerCase + val abiType = System.getProperty("sun.arch.abi", "").toLowerCase + val libPath = System.getProperty("sun.boot.library.path", "").toLowerCase + if (jvmName.startsWith("dalvik") && osName.startsWith("linux")) { + osName = "android" + } else if (jvmName.startsWith("robovm") && osName.startsWith("darwin")) { + osName = "ios" + osArch = "arm" + } else if (osName.startsWith("mac os x") || osName.startsWith("darwin")) { + osName = "macosx" + } else { + val spaceIndex = osName.indexOf(' ') + if (spaceIndex > 0) osName = osName.substring(0, spaceIndex) + } + if (osArch == "i386" || osArch == "i486" || osArch == "i586" || osArch == "i686") osArch = "x86" + else if (osArch == "amd64" || osArch == "x86-64" || osArch == "x64") osArch = "x86_64" + else if (osArch.startsWith("aarch64") || osArch.startsWith("armv8") || osArch.startsWith("arm64")) osArch = "arm64" + else if (osArch.startsWith("arm") && ((abiType == "gnueabihf") || libPath.contains("openjdk-armhf"))) osArch = "armhf" + else if (osArch.startsWith("arm")) osArch = "arm" + Seq(osName + "-" + osArch) } - -} \ No newline at end of file +} diff --git a/src/sbt-test/sbt-javacpp/simple/build.sbt b/src/sbt-test/sbt-javacpp/simple/build.sbt index df26c1c..ad57313 100644 --- a/src/sbt-test/sbt-javacpp/simple/build.sbt +++ b/src/sbt-test/sbt-javacpp/simple/build.sbt @@ -4,4 +4,8 @@ scalaVersion := "2.11.12" javaCppClasses := Seq("javacpp.*") +javaCppVersion := "1.5.1" + +javaCppCustomizer := { builder => builder.compilerOptions(Array("-std=c++11")) } + libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % Test diff --git a/src/sbt-test/sbt-javacpp/simple/project/plugins.sbt b/src/sbt-test/sbt-javacpp/simple/project/plugins.sbt index d82a7fe..fba3ef8 100644 --- a/src/sbt-test/sbt-javacpp/simple/project/plugins.sbt +++ b/src/sbt-test/sbt-javacpp/simple/project/plugins.sbt @@ -5,5 +5,3 @@ |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else addSbtPlugin("org.bytedeco" % "sbt-javacpp" % pluginVersion) } - -libraryDependencies += "org.bytedeco" % "javacpp" % "1.5.1"