I have a multi-project Gradle build. In each subproject I have a properties.gradle file like the following:
def usefulMethod() {
return 'foo'
}
ext {
foo = 'bar'
usefulMethod = this.&usefulMethod
}
And then I import it into the subproject build.gradle using apply from: './properties.gradle'.
However, if two subprojects import a variable with the same name, I get this error:
Cannot add extension with name 'foo', as there is an extension already registered with that name.
It seems that adding to ext affects the entire project instead of just the subproject like I wanted. What is the correct way to import properties and variables from an external file in a subproject without leaking them into the entire project build?
The plain ext is the extension for ENTIRE project, the root project and all subprojects. To avoid polluting the root namespace whenever you include a file via apply from..., you should instead use project.ext. project refers to the current project or subproject being built. For example, the below file could be apply from'd to add a downloadFile function to the current project:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'de.undercouch:gradle-download-task:3.4.3'
}
}
project.ext {
apply plugin: de.undercouch.gradle.tasks.download.DownloadTaskPlugin
downloadFile = { String url, File destination ->
if (destination.exists()) {
println "Skipping download because ${destination} already exists"
} else {
download {
src url
dest destination
}
}
}
}
Related
My build files are large and messy, making them difficult to read. like below:
plugins {
...
id "com.google.protobuf" version "0.8.17"
}
dependencies {
implementation "androidx.datastore:datastore-core:1.0.0"
implementation "com.google.protobuf:protobuf-javalite:3.18.0"
...
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.14.0"
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
I want to define the above code into an external file, and then introduce it into the build file, how should I do it?
According to the Gradle documentation, as of now it is not possible to move the plugin block to other file than the project’s build script or settings.gradle file.
For the other sections, let's say dependencies or protobuf, then you can move these sections on a separate gradle files and import them by using the following statement:
apply from: "${project.rootDir}/your-gradle-file"
Of course the path of your-gradle-file should be adjusted according to the project's folder structure you decide.
If you want to split the dependencies into multiple gradle file you can do the following:
on your main gradle file:
dependencies {
apply from: "${project.rootDir}/depsGroup1.gradle"
apply from: "${project.rootDir}/depsGroup2.gradle"
}
and within each depsGroup file:
dependencies {
implementation xyz
}
I have some code in my settings.gradle file to dynamically create references to version catalogs:
// list of catalogs is actually loaded from a file
def catalogs = [ "com.example:mycatalog:1.0" ]
dependencyResolutionManagement {
versionCatalogs {
catalogs.each { String catalog ->
def catalogName = catalog.split(":")[1] + "Libs"
logger.info "Add version catalog ${catalogName} = ${catalog}"
create(catalogName, { from(catalog) })
}
}
}
And now I want to share that as a Gradle plugin.
I know that I can write a project plugin as a precompiled script plugin with *.gradle files in src/main/groovy directory.
But then Gradle always builds a Plugin<Project> plugin.
How can I declare that I want to a have Plugin<Settings> plugin? Or is this not even possible?
TL;DR
I'm trying to configure two Gradle projects in a way that one project uses files built by the other one.
The first project is added to the second one by includeBuild and the file is defined in the second project as a dependency.
Project testA
settings.gradle:
rootProject.name = 'testA'
build.gradle:
group = 'org.test'
version = '0.0.0.1_test'
task someZip (type: Zip) {
from './settings.gradle'
archiveName = 'xxx.zip'
destinationDir = file("${buildDir}/test")
}
artifacts {
//TODO add something here?
}
Project testB
settings.gradle:
rootProject.name = 'testB'
if (System.getenv('LOCAL_COMPILATION') == 'true') {
includeBuild '../testA'
}
build.gradle:
if (System.getenv('LOCAL_COMPILATION') != 'true') {
repositories {
maven { url '192.168.1.100' }
}
}
configurations {
magic
}
dependencies {
magic 'org.test:xxx:0.0.0.+#zip'
}
task ultimateZip (type: Zip) {
from configurations.magic
archiveName = 'ultimate.zip'
destinationDir = file("${buildDir}/ultimate-test")
}
Description
You may noticed that the example has an option use a maven repository. I wanted to highlight that eventually there will be a possibility to do that.
Using Maven repository is not the point of this question, though, other than the solution should not interfere with that.
(In other words you can assume that System.getenv('LOCAL_COMPILATION') == 'true'.)
The question is how to define the artifact in a way that the other project is able to recognize it.
The preferred solution should be similar to what the Java plugin does because I'm using jar dependencies in my projects and they are working both through includeBuild and through a repository.
The following setup should work (tested with Gradle 5.5.1). It mostly corresponds to your original setup with the exception of the changes indicated by XXX.
Project testA
settings.gradle:
rootProject.name = 'testA'
build.gradle:
group = 'org.test'
version = '0.0.0.1_test'
task someZip (type: Zip) {
from './settings.gradle'
archiveName = 'xxx.zip'
destinationDir = file("${buildDir}/test")
}
// XXX (replaced your empty "artifacts" block)
configurations.create('default')
def myArtifact = artifacts.add('default', someZip) {
name = 'xxx'
}
// XXX (only added to show that publishing works)
apply plugin: 'maven-publish'
publishing {
repositories {
maven { url = 'file:///tmp/my-repo' }
}
publications {
myPub(MavenPublication) {
artifactId myArtifact.name
artifact myArtifact
}
}
}
Project testB
settings.gradle:
rootProject.name = 'testB'
if (System.getenv('LOCAL_COMPILATION') == 'true') {
// XXX (added a dependency substitution to let Gradle know that
// "org.test:xxx" corresponds to the testA project)
includeBuild('../testA') {
dependencySubstitution {
substitute module('org.test:xxx') with project(':')
}
}
}
build.gradle:
if (System.getenv('LOCAL_COMPILATION') != 'true') {
repositories {
// XXX (only changed to show that resolution still works after
// publishing)
maven { url = 'file:///tmp/my-repo' }
}
}
configurations {
magic
}
dependencies {
magic 'org.test:xxx:0.0.0.+#zip'
}
task ultimateZip (type: Zip) {
from configurations.magic
archiveName = 'ultimate.zip'
destinationDir = file("${buildDir}/ultimate-test")
}
As requested in the comments, here’s some more explanation on the created default configuration and the added artifact in project testA.
Composite builds in Gradle currently have the limitation that substituted project dependencies “will always point to the default configuration of the target project”. In your example this means that testA needs to publish in the default configuration. We thus first create the default configuration. Note that some plugins (like java) already create this configuration; you don’t need to create it yourself when using such plugins.
It doesn’t seem to be mentioned explicitly anywhere but as you seem to have found out yourself already, the PublishedArtifacts of a project (as declared with project.artifacts) are important for Gradle to figure out the dependency wiring in composite builds. Hence, we make sure to declare such a PublishedArtifact in testA using this API. The artifact (e.g., its extension) is configured based on the properties of the someZip task. The name seems to not be taken from the someZip task in your example because you manually set archiveName; hence we need to explicitly declare it. If you use archiveBaseName = 'xxx' in someZip instead, then you don’t need the closure when adding the artifact.
In my Gradle build script I want to import a ZIP dependency that contains static analysis configuration (CheckStyle, PMD etc.) and then "apply from" the files in that ZIP. When anyone runs the "check" task, my custom static analysis configuration should be used then.
I've tried the somewhat convoluted solution below, but I can't get it to work. The files are retrieved and unpacked into the "config" directory, but "apply from" does not work - Gradle complains it cannot find the files; I assume this is due to "apply from" being run during the build configuration phase.
Is there a simpler way to do this?
repositories {
maven { url MY_MAVEN_REPO }
}
configurations {
staticAnalysis {
description = "Static analysis configuration"
}
}
dependencies {
staticAnalysis group:'my-group', name:'gradle-static-analysis-conf', version:'+', ext:'zip'
}
// Unzip static analysis conf files to "config" in root project dir.
// This is the Gradle default location.
task prepareStaticAnalysisConf(type: Copy) {
def confDir = new File(rootProject.projectDir, "config")
if (!confDir.exists()) {
confDir.mkdirs()
}
from {
configurations.staticAnalysis.collect { zipTree(it) }
}
into confDir
apply from: 'config/quality.gradle'
}
check.dependsOn('prepareStaticAnalysisConf')
You are perfectly right: Gradle runs apply during evaluation phase, but the prepareStaticAnalysisConf was not executed yet and the archive is not unpacked.
Instead of a task, just write some top-level code. It should do the trick. Also, you'd better use the buildscript level dependency, so that it is resolved before script is executed.
Here is the full script
buildScript {
repositories {
maven { url MY_MAVEN_REPO }
}
dependencies {
classpath group:'my-group', name:'gradle-static-analysis-conf', version:'+', ext:'zip'
}
}
def zipFile = buildscript.configurations.classpath.singleFile
copy {
from zipTree(it)
into 'config'
}
apply from: 'config/quality.gradle'
I've hacked together combination of build.gradle and settings.gradle below for creating an ad-hoc multi-module project out of several single-module projects (e.g., an application and all of its dependencies, or a shared library and everything that uses that library).
settings.gradle:
// find all subprojects and include them
rootDir.eachFileRecurse {
if (it.name == "build.gradle") {
def projDir = it.parentFile
if (projDir != rootDir) {
include projDir.name
project(":${projDir.name}").projectDir = projDir
}
}
}
build.gradle::
// Make sure we've parsed subproject dependencies
evaluationDependsOnChildren()
// Map of all projects by artifact group and name
def declarationToProject = subprojects.collectEntries { p -> [toDeclaration(p), p] }
// Replace artifact dependencies with subproject dependencies, if possible
subprojects.each { p ->
def changes = [] // defer so we don't get ConcurrentModificationExceptions
p.configurations.each { c ->
c.dependencies.each { d ->
def sub = declarationToProject[[group:d.group, name:d.name]]
if (sub != null) {
changes.add({
c.dependencies.remove(d)
p.dependencies.add(c.name, sub)
})
}
}
}
for (change in changes) {
change()
}
}
This works, but it's hard to share -- if somebody else wants to do something similar they have to copy my *.gradle files or cut and paste.
What I'd like to do is take this functionality and encapsulate it in a plugin. The build.gradle part looks easy enough to do in the plugin apply() method, but it seems like the list of subprojects is already set in stone before the plugin gets a chance at it. Is there any way to get in earlier in the build process, e.g. by applying to something other than Project? Or should I resign myself to giving my plugin a task for overwriting settings.gradle?
Solution: Per Peter Niederweiser's answer, I moved the code above into two plugins, one to be called from settings.gradle and the other to be called from build.gradle. In settings.gradle:
buildscript {
repositories { /* etc... */ }
dependencies { classpath 'my-group:my-plugin-project:1.0-SNAPSHOT' }
}
apply plugin: 'find-subprojects'
And in build.gradle:
buildscript {
repositories { /* etc... */ }
dependencies { classpath 'my-group:my-plugin-project:1.0-SNAPSHOT' }
}
evaluationDependsOnChildren()
apply plugin: 'local-dependencies'
Note that calling the plugin from settings.gradle doesn't work in Gradle 1.11 or 1.12 but does work in Gradle 2.0.
You'd need to apply a plugin in settings.gradle, which I believe is supported in recent versions.