How to create multiple (or even one!) directory artifacts from a subproject in gradle 4.5.1

I’m trying to split up a frontend build from our backend build. The frontend build outputs a directory of stuff that needs to be imported into the classpath by the backend. I’d really like to simply do something like this:

artifacts {
smartBuild dir(“$buildDir/somepath/dist”)

staticNodeModules dir("$buildDir/static_node_modules")

}

I would then depend on it in the runtime/testRuntime of my parent project.

runtime project(“:frontend”).smartBuild
runtime project(“:frontend”).staticNodeModules

Being able to separate the project like this would be super awesome, but I can’t figure out how to do this. I’d like gradle to handle the proper dependencies and stuff, have tasks that output those files and hook it all together, but I cannot figure this out from the documentation here: ArtifactHandler - Gradle DSL Version 8.4 There’s something about a DirectoryProvider, but I don’t get it, and using dir() doesn’t seem to work.

I’m having trouble googling for this, because there’s lots about publishing extra jars, or extra files, which is not what I’m aiming for. I really don’t want to “publish” anything, just define it as an output of the sub project, so that gradle can properly figure out the dependencies.

Is this even possible?

1 Like

You need to define configurations for the outputs first, use file() to specify the directory location of those outputs, and use builtBy to declare all tasks that create those outputs. This will look something like this:

configurations {
    smartBuild
    staticNodeModules
}

artifacts {
    smartBuild(file("${buildDir}/somepath/dist")) { builtBy smartBuildTask }
    staticNodeModules(file("${buildDir}/static_node_modules")) { builtBy npmInstall }
}

In the consuming project, you need to specify the path and configuration, not just reference properties in the project, which will resolve the artifacts, running all of the necessary tasks:

runtime project(path: 'frontend', configuration: 'smartBuild')
runtime project(path: 'frontend', configuration: 'staticNodeModules')

Ah! I think it’s the builtBy part that I was missing. I did have the
configurations listed, forgot to include it in the initial post.

Where did you find the stuff about builtBy? I couldn’t find anything like
that anywhere in the docs.

Sadly, this doesn’t seem to work:

  • Where:
    Build file ‘…/frontend/diy/build.gradle’ line: 155

  • What went wrong:
    A problem occurred evaluating project ‘:frontend:diy’.

Could not find method file() for arguments [/…/frontend/diy/build/smart_build/dist, build_4ic4fv92p6coprvrle68dfy0u$_run_closure12$_closure17@605da9a3] on object of type org.gradle.api.internal.artifacts.dsl.DefaultArtifactHandler.

It doesn’t respect the file(...) stuff :frowning:

This is probably also a problem:

  • What went wrong:
    A problem occurred evaluating project ‘:frontend:diy’.

Could not find method dir() for arguments […/frontend/diy/build/smart_build/dist] on project ‘:frontend:diy’ of type org.gradle.api.Project.

the dir method as described here: Directory (Gradle API 8.4) Doesn’t seem to actually exist. Or I haven’t figured out how to use it properly.

Found out that the Directory provider isn’t actually implemented anywhere, except in an internal class, so I might not actually be able to accomplish anything, unless I implement one.

You don’t need a Directory, just the file(...) and an extra set of parenthesis.

I did miss the extra parenthesis the first time, because derp. But that still doesn’t work:

  • What went wrong:
    A problem occurred evaluating project ‘:frontend:diy’.

No signature of method: java.lang.String.call() is applicable for argument types: (java.io.File, build_4ic4fv92p6coprvrle68dfy0u$_run_closure12$_closure18) values: […/frontend/diy/build/static_node_modules, …]
Possible solutions: wait(), any(), trim(), next(), collect(), find()

The only way I was able to make this work:

I extracted the implementation classes from /core/org/gradle/api/internal/file/DefaultProjectLayout.java that implemented Provider<Directory> into my buildSrc and used them as standalone classes.

I was then able to do something like this:

def smartDirResolver = new dir.DefaultDirectoryVar(project.getFileResolver(), "${buildDir}/smart_build/dist")
def staticNodeDirResolver = new dir.DefaultDirectoryVar(project.getFileResolver(), "${buildDir}/static_node_modules")

def smartDir = smartDirResolver.dir("${buildDir}/smart_build/dist")
def staticNodeDir = staticNodeDirResolver.dir("${buildDir}/static_node_modules")

artifacts {
    smartBuild smartDir
    publicNodeBuild staticNodeDir
}

I’m pretty confident that this would give me the output correctly, and it would’ve put things where they were supposed to be. But I’m not certain it would’ve built in the right order. I’ll have to create a standalone project that doesn’t care about spring boot to demo it.

Sadly, I was ultimately thwarted by the Spring Boot plugin, because it expects all dependencies to be Jar files, and so I’ll probably end up just packaging a Jar file anyway :frowning:

If you’re still getting an error, the parenthesis are not quite correct. They either need to be around only the non-closure argument, or there needs to be a comma between the regular argument and closure. I missed this on the original post, but with the edit, the syntax is correct.

You really don’t need a Directory for this to work. This is how I’ve handled front-end resources for years, using just the public APIs as they were intended. You can shortcut the configuration on the consuming side if you’re building a WAR file, but with a JAR, Spring Boot or not, you need it.

The following is a complete, working example of exposing the frontend project artifacts (just a core.css file for the example) to a WAR, JAR, and Spring Boot JAR. In each case, the core.css is located based on where the classpath root is for that archive type.

// settings.gradle
include 'frontend'
include 'jar'
include 'war'
include 'boot'
// frontend/build.gradle
plugins {
    id 'base'
}

configurations {
    webResources
}

task fakeGulpBuildTask {
     outputs.dir("${buildDir}/gulp")
     doLast {
         file("${buildDir}/gulp/core.css").text = 'body { bgcolor: #000000 }'
     }
}

artifacts {
    webResources(file("${buildDir}/gulp")) { builtBy fakeGulpBuildTask }
    // alternatively, comma instead of parenthesis:
    // webResources file("${buildDir}/gulp"), { builtBy fakeGulpBuildTask }
}
// war/build.gradle
plugins {
    id 'war'
}

dependencies {
    runtime project(path: ':frontend', configuration: 'webResources')
}
// jar/build.gradle
plugins {
    id 'java'
}

configurations {
    webResources
}

dependencies {
    webResources project(path: ':frontend', configuration: 'webResources')
}

jar {
    from configurations.webResources
}
// boot/build.gradle
plugins {
    id 'org.springframework.boot' version '1.5.10.RELEASE'
}

repositories {
    jcenter()
}

configurations {
    webResources
}

dependencies {
    webResources project(path: ':frontend', configuration: 'webResources')
}

jar {
    from configurations.webResources
}
// boot/src/main/java/Application.java
public class Application {
    public static void main(String[] args) {
        // just for the Spring Boot Plugin to find a main()
    }
}
1 Like

Hmm, I was pretty sure I did it this way. I’ll give it another go, but it might take a couple days to get back.

Thanks for the detailed example!

Okay, it took me forever to get back to this.

Turns out the one bit I was missing was the webResources configuration on the boot/jar/war project itself, not just on the frontend project, as well as explicitly adding it to the jar.

Thanks for the detailed explanation, I don’t think I’d have ever tried that!