clojure lein jar clash resolution process - jar

I have created a new project with
lein new jar-clash-test
cd jar-clash-test/
I have put the following in project.clj
(defproject jar-clash-test "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.5.0"]
[io.pedestal/pedestal.service "0.1.2"]
...]
:main ^{:skip-aot true} jar-clash-test.core
)
I have put the following in jar-clash-test/src/jar_clash_test/core.clj
(ns jar-clash-test.core
(:require [io.pedestal.service.http :as bootstrap]))
When I run this with
lein repl
I get the following error:
CompilerException java.lang.RuntimeException: No such var: content-type/content-type-response, compiling:(io/pedestal/service/http/ring_middlewares.clj:46:3)
When I look at:
/.m2/repository/io/pedestal/pedestal/0.1.2/pedestal.service-0.1.2/io/ring_middlewares.clj
On line 46 I see:
(leave-interceptor ::content-type-interceptor content-type/content-type-response opts))
Which is defined in the requirements as:
[ring.middleware.content-type :as content-type]
Which means it is trying to bring in the ring-middleware jar.
My hypothesis is that there is a jar version clash for the ring middleware implementation.
This is based on:
[compojure "1.1.3"] [has a dependency]2 on [ring/ring-core "1.1.5"]
[io.pedestal/pedestal.service "0.1.2"] [has a dependency on]3 [ring/ring-core "1.2.0-beta1"]
When I look at:
/.m2/repository/ring/ring-core/1.2.0-beta1/ring-core-1.2.0-beta1/ring/middleware/content_type.clj
The function
(defn content-type-response
exists. When I look at:
/.m2/repository/ring/ring-core/1.1.5/ring-core-1.1.5/ring/middleware/content_type.clj
The function doesn't exist.
My question is - how do I know which version lein has picked up? I can 'assume' it has picked up the earlier one - but how I can know for certain?
My second question is - how can I guarantee which one lein will pick?

You can say lein classpath to get a printout of the computed value of the classpath for your project. Earlier jars win. Another approach: examine the value of (System/getProperty "java.class.path") at the REPL.
If you want to pick a version of an indirect dependency by hand, make it direct, that is, add it to your project.clj; this entry will then override the dependencies' choices in the context of this project. Alternatively, you could add :exclusions to all but one of your dependencies which cause the indirect dependency to be pulled in.

Related

Leiningen Uberjar with JavaFX 17

I have a program written in Clojure and JavaFX back in 2014. A dependency for the program was revised recently to use Java 17. Simply substituting the new version of the dependency produces an error related to not being able to read the new class file format. I would like to update the application but have not been able to generate an uberjar with current versions of Java (17) and JavaFX (17.0.1).
Here are the project.clj and source file for an SSCCE.
(defproject sutest "0.1.0-SNAPSHOT"
:description "Test for including JavaFX components in uberjar"
:dependencies [[org.clojure/clojure "1.10.3"]
[org.openjfx/javafx-controls "17.0.1"]]
:aot :all
:main sutest.core)
(ns sutest.core
(:gen-class
:extends javafx.application.Application)
(:import
[javafx.application Application Platform]
[javafx.event EventHandler]
[javafx.geometry Insets Pos]
[javafx.scene Scene]
[javafx.scene.control Button Label]
[javafx.scene.layout VBox]))
(defn -start [this stage]
(let [hiLbl (Label. "Hello World!")
exitBtn (Button. "Exit")
root (VBox. 12.0)]
(.setOnAction exitBtn (reify EventHandler (handle [_ _]
(Platform/exit))))
(.setPadding root (Insets. 0 10 0 10))
(.addAll (.getChildren root) [hiLbl exitBtn])
(.setAlignment root Pos/CENTER)
(.setScene stage (Scene. root 250 150)))
(.show stage))
(defn -main [& args]
(Application/launch sutest.core args))
The program works as expected when executed directly with lein run or from an IntelliJ IDEA/Cursive "run" configuration. Running lein uberjar completes without errors, but attempting to run the uberjar with
java -jar target/sutest-0.1.0-SNAPSHOT-standalone.jar
produces
Error: JavaFX runtime components are missing, and are required to run this application
I'm using Leiningen 2.9.6 because of this issue with 2.9.7. When running the program, Leiningen builds a classpath containing the needed jars in the local dependency repository.
I've seen a few questions about including the newer JavaFX modules in "fat" jars for Java and tried including them in the Leiningen build. Few related to Clojure and Java. For example, see the project.clj for the fn-fx library. That resorts to a special "leaky" profile to include the modules. That didn't work for me for some reason.
I've tried adding the modules to the IDEA/Cursive project. That correctly downloaded the modules in the project information, but still does not build an uberjar with the modules.
I've fiddled with the "Artifacts" section of the IDEA/Cursive project too. But that was unsuccessful.
There are tutorials on the Gluon site that walk through making a "fat" jar containing the JavaFX components, but those are directed towards Java projects.
Can Leiningen be used to create an uberjar containing the dependencies?
If not Leiningen, how about tools.deps or boot?
Has anyone been successful falling back to a plain Maven build by adapting the Gluon instructions?
After #jewelsea's suggestion, I looked at the contents of the uberjar created in the original question. I used
jar tvf jar tvf target/sutest-0.1.0-SNAPSHOT-standalone.jar
which produced pages and pages of class listings.
Near the top were the lines:
...
306 Sat Oct 23 15:23:46 EDT 2021 javafx-controls-17.0.1.jar
306 Sat Oct 23 15:23:46 EDT 2021 javafx-graphics-17.0.1.jar
302 Sat Oct 23 15:23:46 EDT 2021 javafx-base-17.0.1.jar
746012 Sat Oct 23 15:23:46 EDT 2021 javafx-base-17.0.1-mac.jar
2545243 Sat Oct 23 15:23:48 EDT 2021 javafx-controls-17.0.1-mac.jar
4852153 Sat Oct 23 15:23:48 EDT 2021 javafx-graphics-17.0.1-mac.jar
...
The same jars with the same sizes as used in the classpath built by the lein run command.
So, the JavFX module jars needed were already included in the uberjar.
That triggered a faint memory of a message thread about special treatment of JavaFX applications by the sun.launcher.LauncherHelper class of the java.base module. (The source for the Java 17 version of the class is here if you are interested.) The launcher does a special check of the main class being launched. If it extends javafx.application.Application, the JavaFX modules must be present on the module path, otherwise the error message in the original question is displayed and the launch is aborted. The message thread mentioned above and this StackOverflow answer give a much better explanation.
The net result is that you can't have the main program entry for a JavaFX "fat" jar extend from javafx.application.Application class.
Instead, you can use a "normal" main class that then calls the JavaFX part of the program.
The project listings below are slight edits of those in the original question and implement the solution described.
Here is the revised project.clj. Only the name of the :main class with the program entry point has changed.
(defproject sutest "0.1.0-SNAPSHOT"
:description "Test for including JavaFX components in uberjar"
:dependencies [[org.clojure/clojure "1.10.3"]
[org.openjfx/javafx-controls "17.0.1"]]
:aot :all
:main sutest.launcher)
Here is the new class used to launch the JavaFX portion:
(ns sutest.launcher
(:gen-class)
(:require [sutest.core :as sc]))
(defn -main [& args]
(sc/start-it args))
It calls into the sutest.core namespace with the new start-it function
Here is the revised version of the original JavaFX program. The start-it function launches things instead of the -main function that was used earlier. Note the change to the args parameters though. It no longer has the & rest signature. It must be a (possibly empty) vector of strings. The names of the variables for the label and button have been changed to reflect Clojure variable naming conventions.
(ns sutest.core
(:gen-class
:extends javafx.application.Application)
(:import
[javafx.application Application Platform]
[javafx.event EventHandler]
[javafx.geometry Insets Pos]
[javafx.scene Scene]
[javafx.scene.control Button Label]
[javafx.scene.layout VBox]))
(defn -start [_ stage]
(let [hi-lbl (Label. "Hello World!")
exit-btn (Button. "Exit")
root (VBox. 12.0)]
(.setOnAction exit-btn (reify EventHandler (handle [_ _]
(Platform/exit))))
(.setPadding root (Insets. 0 10 0 10))
(.addAll (.getChildren root) [hi-lbl exit-btn])
(.setAlignment root Pos/CENTER)
(.setScene stage (Scene. root 250 150)))
(.show stage))
(defn start-it [args]
(Application/launch sutest.core args))
That's it. Now you can build an uberjar and run it from the command line. Also, it can still be run directly from Leiningen with the lein run command and from within the IDE (after changing the name of the class with the entry point in the run configuration.)
This is a dirty hack
It is based on detailed knowledge of the internals of the launcher rather than using modules. When launched, the program still gives a warning like:
WARNING: Unsupported JavaFX configuration: classes were loaded from 'unnamed module #25b2b199'
Hopefully it will work long enough to figure out how to use Java modules in "fat" jars in Clojure though.

Defining plugin dependency between subprojects in SBT?

EDIT:
Since I put up the bounty, I thought I should restate the question
How can a SBT project P, with two sub-projects A and B, set up B to have a plugin dependency on A, which is a SBT plugin?
Giving P a plugin dependency on A does not work, since A depends on other things in P, which results in a circular dependency graph
It has to be a plugin dependency, for A is a plugin needed to run Bs test suite.
dependsOn doesn't work, because, well, it has to be a plugin dependency
I'd like to know either of
How to do this, or
Why this is impossible, and what the next best alternatives are.
EDIT: clarified that it's a plugin-dependency, since build-dependency is ambiguous
When you have a multi-project build configuration with "project P and two sub-projects A and B" it boils down to the following configuration:
build.sbt
lazy val A, B = project
As per design, "If a project is not defined for the root directory in the build, sbt creates a default one that aggregates all other projects in the build." It means that you will have an implicit root project, say P (but the name is arbitrary):
[plugin-project-and-another]> projects
[info] In file:/Users/jacek/sandbox/so/plugin-project-and-another/
[info] A
[info] B
[info] * plugin-project-and-another
That gives us the expected project structure. On to defining plugin dependency between B and A.
The only way to define a plugin in a SBT project is to use project directory that's the plugins project's build definition - "A plugin definition is a project in <main-project>/project/." It means that the only way to define a plugin dependency on the project A is to use the following:
project/plugins.sbt
addSbtPlugin("org.example" % "example-plugin" % "1.0")
lazy val plugins = project in file(".") dependsOn(file("../A"))
In this build configuration, the plugins project depends on another SBT project that happens to be our A that's in turn a plugin project.
A/build.sbt
// http://www.scala-sbt.org/release/docs/Extending/Plugins.html#example-plugin
sbtPlugin := true
name := "example-plugin"
organization := "org.example"
version := "1.0"
A/MyPlugin.scala
import sbt._
object MyPlugin extends Plugin
{
// configuration points, like the built in `version`, `libraryDependencies`, or `compile`
// by implementing Plugin, these are automatically imported in a user's `build.sbt`
val newTask = taskKey[Unit]("A new task.")
val newSetting = settingKey[String]("A new setting.")
// a group of settings ready to be added to a Project
// to automatically add them, do
val newSettings = Seq(
newSetting := "Hello from plugin",
newTask := println(newSetting.value)
)
// alternatively, by overriding `settings`, they could be automatically added to a Project
// override val settings = Seq(...)
}
The two files - build.sbt and MyPlugin.scala in the directory A - make up the plugin project.
The only missing piece is to define the plugin A's settings for the project B.
B/build.sbt
MyPlugin.newSettings
That's pretty much it what you can do in SBT. If you want to have multi-project build configuration and have a plugin dependency between (sub)projects, you don't have much choice other than what described above.
With that said, let's see if the plugin from the project A is accessible.
[plugin-project-and-another]> newTask
Hello from plugin
[success] Total time: 0 s, completed Feb 13, 2014 2:29:31 AM
[plugin-project-and-another]> B/newTask
Hello from plugin
[success] Total time: 0 s, completed Feb 13, 2014 2:29:36 AM
[plugin-project-and-another]> A/newTask
[error] No such setting/task
[error] A/newTask
[error] ^
As you may have noticed, newTask (that comes from the plugin from the project A) is available in the (default) root project and the project B, but not in A.
As Jacek said, it cannot be done as I would like, as a subproject cannot have a SBT plugin that the root project does not. On the other hand, this discussion on the mailing list contains several alternatives, and would no doubt be useful to anyone who comes across this question in the future.
EDIT: Well, in the end the alternatives mentioned (sbt scripted, etc) were hard and clunky to use. My final solution was to just have a separate project (not subproject) inside the repo that depends on the original project via it's ivy coordinates, and using bash to publishLocal the first project, going into the second project and running its tests
sbt publishLocal; cd test; sbt test; cd ..
I always thought the point of something like SBT was to avoid doing this kind of bash gymnastics, but desperate times call for desperate measures...
This answer may include the solution https://stackoverflow.com/a/12754868/3189923 .
From that link, in short, set exportJars := true and to obtain jar file paths for a (sub)project exportedProducts in Compile.
Leaving the facts about plugins by side, you have a parent project P with sub-projects A and B. And then you state that A depends on P. But P is a aggregate of A and B and hence depends on A. So you already have a circular dependency between A and P. This can never work.
You have to split P in two parts: The part where A depends on (let's call this part A') and the rest (let's call this P_rest). Then you throw away P and make a new project P_rest consisting of A', A and B. And A depends on A'.

sbt key that corresponds to command that I type in

I want to make my tests run every time I type universal:package-zip-tarball. I know that to do this, I have to put something like
someKey <<= someKey dependsOn (test in Test)
in my project/Build.scala, where someKey is the key that provides the task I want to depend on the test run, in this case, universal:package-zip-tarball.
But my generic question is: how do I find out what someKey should be?
Note that this is a Play framework project, and I don't even know if universal:package-zip-tarball is provided by Play, or by some other sbt plugin.
Is there any way sbt can just tell me, without me having to go searching for the source code repository containing the relevant code?
Use the inspect command:
$ inspect universal:package-zip-tarball
[...]
[info] Defined at:
[info] (com.typesafe.sbt.packager.universal.UniversalPlugin)
UniversalPlugin.scala:73
This is actually the location of the definition of the code of the task, but this is close enough to help, because it lets us find the key (the key will be in the same sbt plugin).
From this we can find out that the key is:
com.typesafe.sbt.packager.universal.Keys.packageZipTarball
Unfortunately, just substituting this in doesn't work - it says:
[error] Reference to undefined setting:
[error]
[error] my-project/*:packageZipTarball from my-project/*:packageZipTarball
[error] Did you mean my-project/universal-docs:packageZipTarball ?
[error]
[error] Use 'last' for the full log.
So to fix this, the only thing remaining is to translate the universal: prefix. It is in fact this:
packageZipTarball in Universal <<= packageZipTarball in Universal dependsOn (test in Test)
but it just needs an extra import to make it compile:
import com.typesafe.sbt.SbtNativePackager._
(In this case, SbtNativePackager is the main plugin object, I think. Other plugins might require importing something else, to translate such a prefix.)

How do you do develop an SBT project, itself?

Background: I've got a Play 2.0 project, and I am trying to add something to do aspectj weaving using aspects in a jar on some of my classes (Java). (sbt-aspectj doesn't seem to do it, or I can't see how). So I need to add a custom task, and have it depend on compile. I've sort of figured out the dependency part. However, because I don't know exactly what I'm doing, yet, I want to develop this using the IDE (I'm using Scala-IDE). Since sbt projects (and therefore Play projects) are recursively defined, I assumed I could:
Add the eclipse plugin to the myplay/project/project/plugins.sbt
Add the sbt main jar (and aspectj jar) to myplay/project/project/build.sbt:
libraryDependencies ++= Seq(
"org.scala-sbt" % "main" % "0.12.2",
"aspectj" % "aspectj-tools" % "1.0.6"
)
Drop into the myplay/project
Run sbt, run the eclipse task, then import the project into eclipse as a separate project.
I can do this, though the build.scala (and other scala files) aren't initially considered source, and I have to fiddle with the build path a bit. However, even though I've got the sbt main defined for the project, both eclipse IDE and the compile task give errors:
> compile
[error] .../myplay/project/Build.scala:2: not found: object keys
[error] import keys.Keys._
[error] ^
[error] .../myplay/project/SbtAspectJ.scala:2: object Configurations is not a member of package sbt
[error] import sbt.Configurations.Compile
[error] ^
[error] .../myplay/project/SbtAspectJ.scala:3: object Keys is not a member of package sbt
[error] import sbt.Keys._
[error] ^
[error] three errors found
The eclipse project shows neither main nor aspectj-tools in its referenced-libraries. However, if I give it a bogus version (e.g. 0.12.4), reload fails, so it appears to be using
the dependency.
So,...
First: Is this the proper way to do this?
Second: If so, why aren't the libs getting added.
(Third: please don't let this be something dumb that I missed.)
If you are getting the object Keys is not a member of package sbt error, then you should check that you are running sbt from the base directory, and not the /project directory.
sbt-aspectj
sbt-aspectj doesn't seem to do it, or I can't see how.
I think this is the real issue. There's a plugin already that does the work, so try making it work instead of fiddling with the build. Using plugins from build.scala is a bit tricky.
Luckily there are sample projects on github:
import sbt._
import sbt.Keys._
import com.typesafe.sbt.SbtAspectj.{ Aspectj, aspectjSettings, compiledClasses }
import com.typesafe.sbt.SbtAspectj.AspectjKeys.{ binaries, compileOnly, inputs, lintProperties }
object SampleBuild extends Build {
....
// precompiled aspects
lazy val tracer = Project(
"tracer",
file("tracer"),
settings = buildSettings ++ aspectjSettings ++ Seq(
// stop after compiling the aspects (no weaving)
compileOnly in Aspectj := true,
// ignore warnings (we don't have the sample classes)
lintProperties in Aspectj += "invalidAbsoluteTypeName = ignore",
// replace regular products with compiled aspects
products in Compile <<= products in Aspectj
)
)
}
How do you do develop an SBT project, itself?
If you're interested in hacking on the build still the first place to go is the Getting Started guide. Specifically, your question should be answered in .scala Build Definition page.
I think you want your build to utilize "aspectj" % "aspectj-tools" % "1.0.6". If so it should be included in myplay/project/plugins.sbt, and your code should go into myplay/project/common.scala or something. If you want to use IDE, you have have better luck with building it as a sbt plugin. That way your code would go into src/main/scala. Check out sbt/sbt-aspectj or sbt/sbt-assembly on example of sbt plugin structure.

CompilerException java.lang.IllegalArgumentException: Unable to resolve classname: Carro, compiling:(carro/car.clj:6)

So.. I have this problem (the one in the title). Just to give a background about what I did:
Create a Java class called Carro:
public class Carro{
public Carro(){}
public void turnon(String sound){
System.out.println(sound);
}
}
I've compile it:
javac Carro.java
And created a .jar:
jar -cf Carro.jar Carro.class
So, I created a new lein project:
lein new test
Created a /lib directory and pasted the Carro.jar in it.
Create a folder called carro in test/src/ directory and create a .clj file, called car.clj:
(ns carro.car
(:import [Carro] )
)
(defn callCarro []
(let [car (new Carro)]
(.turnon "vruuum!" car)
)
)
After all of that, I edited the project.clj file and add a :import [Carro] after the last parenthesis.
So, when I run the project using lein repl, I get this error:
$ lein repl
user=> (require 'carr.car :reload)
CompilerException java.lang.IllegalArgumentException:
Unable to resolve classname: Carro, compiling:(carro/car.clj:6)
Any ideas to solve this... Problem?
Leiningen 2 relies on maven to manage dependencies. Version 1 used to copy jars to the lib directory but this behavior was removed (see https://github.com/technomancy/leiningen/blob/stable/doc/FAQ.md). If you need a certain jar for your project, the easiest way is to install that jar on your local maven repo and let leiningen do the rest.
Maven (and so leiningen) needs a version, a group and an artifact for the jar you are working with (see https://github.com/technomancy/leiningen/blob/master/doc/TUTORIAL.md#artifact-ids-groups-and-versions). So our first step will be to rename Carro.jar to Carro-0.1.0.jar. To make things a little easier, lets use a leiningen plugin to do the rest. Add the lein-localrepo plugin to your project.clj, something like:
(defproject foo "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.4.0"]]
:plugins [[lein-localrepo "0.4.1"]])
Then you can ask leiningen for the coordinates of the Carro dependency:
$ lein localrepo coords Carro-0.1.0.jar
Carro-0.1.0.jar Carro/Carro 0.1.0
That last part is the info needed to install to the mvn repo:
$ lein localrepo install Carro-0.1.0.jar Carro/Carro 0.1.0
After that,the jar is installed in your mvn repository: ~/.m2/repository/Carro/Carro/0.1.0/Carro-0.1.0.jar. So finally, add the newly installed dependency to project.clj:
(defproject foo "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.4.0"]
[Carro/Carro "0.1.0"]]
:plugins [[lein-localrepo "0.4.1"]])
And then from the repl:
$ lein repl
user=> (new Carro)
#<Carro Carro#7c04703c>
I suspect there may be multiple causes:
Clojure has trouble loading classes without a package which causes this.
that is not the typical location for test classes if you are using Leiningen
If you didn't run (import 'Classname) from the repl you will need to do that as well.

Resources