Mixed Clojure and ClojureScript Build With Clojurephant - gradle

I'm considering moving a project from boot to Gradle with clojurephant hoping to leverage more of the Gradle ecosystem. This project builds one large uberjar that contains a Clojure project with Ring and Jetty that in turn ships a ClojureScript app built with re-frame.
In boot, I essentially just required boot-cljs, added
(boot-cljs/cljs :optimizations :advanced)
to my build task, which also calls (pom), (aot) and (uber) (all standard boot tasks), and everything worked smoothly.
With Clojurephant, I find that the Clojure and ClojureScript parts end up in different subdirectories. In particular I find underneath of build
clojure/main
clojurescript/main
resources/main (essentially a copy of my resources project folder)
Adding to my confusion is that these paths don't translate in a way that I can see to the structure of the Uberjar that Gradle builds using the shadow plugin
Some excerpts from my build.gradle:
plugins {
id 'dev.clojurephant.clojure' version '0.5.0'
id 'dev.clojurephant.clojurescript' version '0.5.0'
id 'application'
id 'com.github.johnrengelman.shadow' version '5.0.0'
id 'maven-publish'
id 'distribution'
id 'com.meiuwa.gradle.sass' version '2.0.0'
}
// ...
clojure {
builds {
main {
aotAll()
}
}
}
// ...
clojurescript {
builds {
all {
compiler {
outputTo = 'public/app.js'
outputDir = 'public/js/out'
main = 'com.example.mycljsstuff'
assetPath = 'js/out'
}
}
main {
compiler {
optimizations = 'advanced'
sourceMap = 'public/app.js.map'
}
}
dev {
compiler {
optimizations = 'none'
preloads = ['com.example.mycljsstuff']
}
}
}
}
EDIT: Forgot to mention that for boot I configure the init function to start loading the CLJS code in a file called app.cljs.edn. With Clojurephant I only found a way to set a main namespace, not a function.
My question here is ultimately, how can I configure the ClojureScript build so that it works in the end when being delivered from the Uberjar?
The Clojure things seem to work. Ring and Jetty run and happily deliver a first static webpage. But all the CLJS/JS things can't be found.
I'd be super happy to just receive some pointers to other projects where I can learn, documentation, or tutorials. I haven't found a lot and then got lost in understanding the code of Clojurephant itself.

A year ago at work I was able to split up a combined CLJ & CLJS (backend/frontend) project into 2 separate projects: pure Clojure for the backend, and pure ClojureScript for the frontend. This resolved many, many problems we had and I would never try to keep two codebases in the same project again.
The backend CLJ part continued to use Lein as the build tool. It's not perfect but it is well understood.
The frontend CLJS part was transitioned from the original Figwheel (aka "1.0") to the newer Figwheel-Main (aka "2.0"). Following the lead from figwheel.org
we chose to restructure the build into using Deps/CLI (the original combined project used Lein for everything). The transition away from Lein to Deps/CLI was a real winner for CLJS work.
While Deps/CLI works great for pure Clojure code, be aware that it does not natively support the inclusion of Java source code. I have a template project
you can clone that shows a simple workaround for this.
For any CLJS project, I highly recommend the use of Figwheel-Main over the original Figwheel as it is a major "2.0" type of upgrade, and will make your life much, much better.
Enjoy!

Trying to answer my own question here, since I managed to get it running.
Clojure
plugins {
id 'dev.clojurephant.clojure' version '0.5.0'
id 'application'
id 'com.github.johnrengelman.shadow' version '5.0.0'
// ... more to come further down
}
group = 'com.example'
version = '1.0.0-SNAPSHOT'
targetCompatibility = 1.8
mainClassName = 'com.example.myproject'
dependencies {
implementation(
'org.clojure:clojure:1.10.1',
'ring:ring:1.8.0',
// and many more
)
testImplementation(
'junit:junit:4.12',
'clj-http-fake:clj-http-fake:1.0.3',
'ring:ring-mock:0.4.0'
)
devImplementation(
'org.clojure:tools.namespace:0.3.0-alpha4',
'cider:cider-nrepl:0.21.1',
'org.clojure:java.classpath',
'jonase:eastwood:0.3.11',
'lein-bikeshed:lein-bikeshed:0.5.2'
)
}
clojure {
builds {
main {
aotAll()
}
}
}
clojureRepl {
handler = 'cider.nrepl/cider-nrepl-handler'
}
This is enough to get an executable JAR running -main from com.example.myproject namespace when calling ./gradlew shadowJar from the command line. Not sure if the application plugin is relevant here. Also, ./gradlew clojureRepl spins up an nrepl that Emacs/Cider can connect to.
ClojureScript
// Merge with plugins above. Reproduced only the CLJS relevant
// part here
plugins {
id 'dev.clojurephant.clojurescript' version '0.5.0'
}
// Again, merge with dependencies above
dependencies {
implementation(
// ....
'org.clojure:clojurescript:1.10.597',
're-frame:re-frame:0.10.5',
'reagent:reagent:0.7.0',
// and a few more
)
}
clojurescript {
builds {
all {
compiler {
outputTo = 'public/app.js'
outputDir = 'public/state/'
main = 'com.example.myproject.webui'
assetPath = 'status/app.out'
}
}
main {
compiler {
optimizations = 'advanced'
sourceMap = 'public/app.js.map'
}
}
dev {
compiler {
optimizations = 'none'
preloads = ['com.example.myproject.webui']
}
}
}
}
This creates a /public folder in the top level of there JAR and app.js inside that folder, which is where the HTML file delivered by Ring expects it.
One important step for me was to call my init CLJS function in the CLJS file which was taken care of by some other component before. I'm not sure that this set up is totally correct and will do the figwheel setup eventually. Maybe the call to init will not be necessary then.
CSS
// Merge ....
plugins {
id 'com.meiuwa.gradle.sass' version '2.0.0'
}
sassCompile {
output = file("$buildDir/resources/main/public/")
source = fileTree("${rootDir}/src/main/resources/public/")
include("**/*.scss")
exclude("**/_*.sass", "**/_*.scss")
}
This compiles my app.scss to app.css in the right spot, where my HTML file searches for it.
Pro & Con
After migrating, I get for free
Faster compilation locally and in CI after setting up the caches correctly.
License and OWASP dep check reports by using plugins com.github.jk1.dependency-license-report and org.owasp.dependencycheck for which equivalents exists in leiningen but not boot (AFAIK).
Maven publishing with authentication via HTTP header instead of username/password which is not available in boot.
On the downsides:
Yucky syntax in my build file. No, really.
I have to enter the nrepl port number in Emacs manually.

Related

Publishing test fixtures with submodules, gradle

I have been looking around for a way to include test fixtures in my gradle publications.
https://developer.android.com/studio/publish-library/configure-test-fixtures#kts suggests that it should work automatically so long as the project name is set correctly, which I have done in the settings.gradle file. This seems to solve the issue in the case of https://github.com/slackhq/EitherNet/issues/44.
For context, my project is built with several sub modules and I have defined a custom publication for each (I suspect this is the clue to the issue) as shown here:
subprojects {
// ... some repos and unimportant plugin applications
tasks {
register("prepareKotlinBuildScriptModel") {}
withType<BootJar> {
enabled = false // this is enabled in the jar I wish to be bootable
}
withType<Test> {
useJUnitPlatform()
}
getByName<Jar>("jar") {
enabled = true
archiveClassifier.set("")
}
}
publishing {
publications {
create<MavenPublication>(project.name) {
version = projectVersion
artifactId = tasks.jar.get().archiveBaseName.get()
groupId = "${projectGroup}.${rootProject.name}"
from(components["kotlin"])
}
}
}
For ref, this is currently what my module structure and build.gradle looks like for the module in question:
module structure
plugins {
id("java-test-fixtures")
id("java-library")
}
dependencies {
testFixturesApi(project(":model"))
... unrelated stuff
The test fixtures work fine as internal dependencies in the project itself, but they do not get published so that they can be used in external projects.
So my question is if there is a way to bake the test fixtures into my submodule jars so they can be used in external projects?
Any input would be highly appreciated.
Tried, expected, result:
Publishing to local repo, expected the test fixtures to be bundled, they were not.

Gradle Idea plugin - issues with specifing test sources

I'm trying to create a custom source set and mark its contents in Intellij Idea as a Test Sources Root. I tried to use idea plugin and do it according to the gradle official website but it is not clear for me how it works.
First of all the documentation specifies the following configuration setup
idea {
module {
testSources.from(sourceSets["intTest"].java.srcDirs)
}
}
When I try to use it i receive Unresolved reference: testSources. Where is it coming from?
Then I tried to use:
idea {
module {
testSourceDirs = intTest.java.srcDirs
}
}
it works fine as long as I use only Java. After applying Kotlin plugin however, both kotlin and java + resources folder are again treated as Sources Root not Test Sources. To fix that I had to change from:
testSourceDirs = intTest.java.srcDirs
to:
testSourceDirs = intTest.kotlin.srcDirs
and now all folders are Test Source Root again. Since kotlin.srcDirs also includes java.srcDirs it looks like you have to specify all, otherwise it is ignored...
Now the real issue came when I used gradle-avro-plugin. Applying it made my folders marked as Sources Root again. I believe it is because it adds another avro directory, but just to main source set.
Does anyone know how to make it marked as Test Sources having both kotlin and avro plugin applied? Am I doing something wrong here? Beacause this beheviour seems to be buggy in the first place.
Tested with:
IntelliJ IDEA 2022.3.1 (Ultimate Edition)
Gradle 6.8.3 and 7.4.2
Plugin id("com.github.davidmc24.gradle.plugin.avro") version "1.5.0"
Plugin kotlin("jvm") version "1.7.0"

Gradle7 Version Catalog: How to use it with buildSrc?

I am very excited about the incubating Gradle's version catalogs and have been experimenting with it. I’ve found that the information in my gradle/libs.versions.toml is accessible in the build.gradle.kts scripts for my app and utility-lib projects.
However, I am unable to use the content of the toml file for buildSrc/build.gradle.kts or the convention files.
The only way that I could build was to hard-code the dependencies into those files, as I did before the version catalog feature.
In the buildSrc folder, I created a settings.gradle.kts file and inserted the dependencyResolutionManagement code for versionCatalogs, which is pointing to the same file as for my app and utility-lib projects.
Based on the Gradle7 docs, it seems that sharing a version catalog with buildSrc and modules is possible… I’d appreciate a nudge into getting it to work with buildSrc, if possible.
Here is a simple sample project, which I created via gradle init: my-version-catalog
Thank you for your time and help,
Mike
With Gradle 7.3.3, it is possible. Note version catalogs are GA since Gradle 7.4
The code snippet assumes Gradle is at least 7.4, but if you need them prior that version, insert enableFeaturePreview("VERSION_CATALOGS") at the beginning of each settings.gradle.kts.
Using buildSrc
buildSrc/settings.gradle.kts
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
buildSrc/build.gradle.kts
dependencies {
implementation(libs.gradleplugin.intellij) // <- the lib reference
}
You can even use the version catalog for plugins
gradle/libs.versions.toml
...
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
jetbrains-changelog = { id = "org.jetbrains.changelog", version.ref = "changelog-plugin" }
jetbrains-intellij = { id = "org.jetbrains.intellij", version.ref = "intellij-plugin" }
hierynomus-license = { id = "com.github.hierynomus.license", version.ref = "license-plugin" }
nebula-integtest = { id = "nebula.integtest", version.ref = "nebula-integtest-plugin" }
build.gradle.kts
plugins {
id("java")
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.nebula.integtest)
alias(libs.plugins.jetbrains.intellij)
alias(libs.plugins.jetbrains.changelog)
alias(libs.plugins.hierynomus.license)
}
Note for accessing the catalog within scripts, please refer to the below section, the trick is the same.
Using convention plugins and included build
In the main project include a the Gradle project that holds the convention plugins.
build.gradle.kts
includeBuild("convention-plugins") // here it's a subfolder
convention-plugins/settings.gradle.kts
dependencyResolutionManagement {
repositories {
gradlePluginPortal()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "convention-plugins"
The trick to enable convention plugins to access the version catalog is split in two part, add an ugly implementation dependency that locate where the version catalog generated classes are located.
libs.javaClass.superclass.protectionDomain.codeSource.location
Then in the convention plugin refer to the libs extension via Project::the.
val libs = the<LibrariesForLibs>()
This is tracked by gradle/gradle#15383.
convention-plugins/build.gradle.kts
plugins {
`kotlin-dsl`
}
dependencies {
implementation(libs.gradleplugin.kotlin.jvm)
// https://github.com/gradle/gradle/issues/15383
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
}
And in the actual convention plugin
import org.gradle.accessors.dm.LibrariesForLibs
plugins {
id("org.jetbrains.kotlin.jvm")
}
// https://github.com/gradle/gradle/issues/15383
val libs = the<LibrariesForLibs>()
dependencies {
detektPlugins(libs.bundles.kotlinStuff) // access catalog entries
}
The org.gradle.accessors.dm.LibrariesForLibs class is generated by gradle is somewhere in local gradle folder ./gradle/<version>/dependency-accessors/<hash>/classes
Quick note that older IntelliJ IDEA currently (2022.3) reports alias(libs.gradleplugin.thePlugin) as an error in the editor,
although the dependencies are correctly resolved.
This tracked by KTIJ-19369, the ticket indicates this is actually a bug in Gradle Kotlin DSL gradle/gradle#22797, and someone made a simple IntelliJ IDEA plugin to hide this error until resolved.
Brice, it looks like a can of worms to go down that path, particularly for my situation, where I'm trying to use a libs.version.toml file from an android project, but the custom plugin is of course from a java/kotlin project. I tried creating the libs file by hardwiring the path to the toml file in the custom plugin. It might work if both were java projects, but I never tried that since that's not what I'm after. The ideal solution would be for the plugin to use the libs file from the project it is applied to, but it looks like the version catalog needs to be created in the settings file, before you even have access to "Project", so that's why you would have to hardwire the path.
Short answer. No, but there are other techniques for a custom plugin to get project version data from the project it is applied to.

Android Studio 2.2's incremental compiler can't see generated protobufs

I upgraded my stable version of Android Studio to 2.2 and now the IDE's "incremental compiler" can't find any of the symbols for generated protobuf classes. I open the project and it can build and deploy the app to a device just fine. But when I open a Java class file that contains generated protobuf references, Android Studio marks them as errors soon after the file opens. Every Java import of a generated proto class is marked with "Cannot resolve symbol".
I first noticed this a month ago on the canary channel but didn't think anything of it because I was floundering with other protobuf issues (upgrading to 3.0.0 with its javalite split). I forgot about it until today. It's still possible to work on the project, it's just that the IDE is near useless since it thinks there are errors (even though real compiles are fine with it).
For reference.
gradle-2.14.1
com.android.tools.build:gradle:2.2.0
com.google.protobuf:protobuf-gradle-plugin:0.8.0
com.google.protobuf:protobuf-lite:3.0.0
com.google.protobuf:protoc:3.0.0
com.google.protobuf:protoc-gen-javalite:3.0.0
And in the modules that contain .proto files:
protobuf {
protoc {
artifact = google_protoc_artifact
}
plugins {
javalite {
artifact = google_protoc_javalite_artifact
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
remove java
}
task.plugins {
javalite { }
}
}
}
}
We had the same issue and found out the following:
1) In order for idea (studio) to see your source, you need to help it by adding the idea plugin to your module:
apply plugin: 'idea'
idea {
module {
// Use "${protobuf.generatedFilesBaseDir}/main/javalite" for LITE_RUNTIME protos
sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/java");
}
}
2) Another bug, Android Studio seems to ignore any source directory under build/. You have to move your generated directory outside of build:
protobuf {
generatedFilesBaseDir = "$projectDir/src/generated"
}
These two changes fixed the problem introduced by Android Studio 2.2 for us.
In my case, I was using the kotlin protobuf plugin and to fix the error of the IDE not being able to resolve it. I tweaked the other answer above to point to the main folder.
// build.gradle.kts
plugins {
idea
}
idea {
module {
// explicitly tell intellij where to resolve generated proto files
sourceDirs.plusAssign(file("build/generated/source/proto/main/"))
}
}

Gradle: assemble output of a specific configuration

For a project, I have a custom configuration which should simply extend the default one, both in terms of java sources and in terms of dependencies.
This is what it looks like at the moment:
configurations {
// Tools inherits everything from default and adds on
toolsCompile.extendsFrom(compile)
}
sourceSets {
main {
java {
srcDirs = ['src']
}
}
tools {
java {
// Tools extends on the core sources
srcDirs = sourceSets.main.java.srcDirs + ['src-tools']
}
}
}
dependencies {
compile libs.a
compile libs.b
compile libs.c
// Tools adds on the dependencies of the default configuration
toolsCompile libs.d
toolsCompile libs.e
}
I know I could have also used sub-projects for this. I gave up on it, after trying, because I can't get it work properly together with the Eclipse integration plugin (it works fine when used from command line).
I have a couple of questions on the solution above:
Is the way I extend tools.java.srcDirs correct? Is there a more elegant way?
EDIT: Apparently it is not correct, as gradle eclipse generates a .classpath with a duplicate entry for src. Help please?
After I created my tools configuration, I know I can for example use it as a dependency from another project, as in compile project(path: ':myproject', configuration: 'tools'). What do I need to add if I instead want to get the output of the assemble task for my tools configuration? Do I have to make an explicit task for that? The task toolsClasses is created automatically, but not toolsAssemble or toolsBuild.

Resources