Configuration cache migration: access to subproject in multi-project layout

Currently i am migrating all tasks and build logic to be Gradle configuration cache compliant.

Mostly it’s been clear what to change, but now i am struggling on finding the correct solution.

I have the following task

tasks.register 'printJiraVersionComment', {
	onlyIf {
		BuildInfo.isEnvIde()
	}
	description = 'Prints all components with their next version to be inserted in a Jira comment'

	def final projects = [
			project(':projectA'),
			project(':projectB'),
	]

	doLast {
		logger.lifecycle '*#fixed*'

		projects.each {
			def nextVersion = VersionUtils.incrementVersion(it.version as String)
			logger.lifecycle "|${it.name}|${nextVersion}|"
		}
	}
}

Which basically gets the next version numbers and prints a comment on console to be copy/pasted to Jira.

How, can I achieve the same in a configuration cache friendly way?

Running without changes, give me

11 problems were found reusing the configuration cache, 1 of which seems unique.

  • Task :printJiraVersionComment of type org.gradle.api.DefaultTask: cannot deserialize object of type ‘org.gradle.api.Project’ as these are not supported with the configuration cache.

which is clear.

But getting the sub-projects versions upfront does not work. Even wrapping it in providers did not help.

Yeah, you cannot access the configuration model at task execution time with CC.

The underlying problem that you should solve though is, that you do cross-project access at all.
Even at configuration time you should not access the model of other projects.
This is almost as bad as doing cross-project configuration and introduces project coupling which disturbs more sophisticated Gradle optimizations and features.
For example latest when isolated projects is a thing you will not be able to do this.

It is better to have in projectA and projectB a printJiraVersionComment task that then does it per project, for example by having a convention plugin that does the logic and gets applied to the respective projects.

OK, i found a version that does work without complaints.

But is that the way how it’s supposed to be? o.O

tasks.register 'printJiraVersionComment', {
    // ...

	def final versions = [:]

	[
			'projectA'             : ':projectA',
			'projectB'             : ':projectB'
	].each { projectName, projectPath ->
		project projectPath, { p ->
			versions.put projectName, p.version
		}
	}

	doLast {
		logger.lifecycle '*#fixed*'

		versions.each { projectName, projectVersion ->
			def nextVersion = VersionUtils.incrementVersion(projectVersion as String)
			logger.lifecycle "|${projectName}|${nextVersion}|"
		}
	}

Alternatively, but much more involved would be adding a task to the subprojects that writes the information to a file that is then published using an outgoing configuration and then have dependencies to those projects that resolve that variant and read the information from those files in the one task that does the printing. But that is most probably too much overhead for the task at hand and would also require modifying the projects so you could as well just add the task that does the printing to each project unless you require it as one combined message.

But is that the way how it’s supposed to be? o.O

No, as you still do cross-project access and for example also might miss changes to the versions that the project buildscripts are doing. :slight_smile:

Having a separate task at each project, does not meet our expectations.

This ends up in copy&pasting several outputs and combining it manually.

We are kinda lazy and have about 12 projects’ version to be gathered.

So, we want Gradle to combine it :slight_smile:

If it’s not recommended to “misuse” Gradle’s API for that, I would rather fileTree the sub-projects’ build scripts and parse their versions by hand.

Ok, get it. The cross-project access is the problem.

But why is the project method even here, if it’s not supposed to be used for that?

This ends up in copy&pasting several outputs and combining it manually.

I already suggested how to do it then cleanly. :slight_smile:
Have the projects write the necessary information to a file.
Make that file an attribute of an outgoing variant.
Resolve to these variants in the root project and feed them to the task.
Then in the task read the information from the files, build the message and print it. :slight_smile:

But why is the project method even here, if it’s not supposed to be used for that?

Backwards compatibility.
That method is there for veeery long already. :slight_smile:
Just like allprojects { ... }, subprojects { ... }, project(...) { ... } and other ways to do cross-project configuration or model access that should not be used “nowadays”. :slight_smile:

Thanks for the clarification :+1:

I actually use these APIs quite a lot XD

  • allprojects
  • subprojects
  • project

Can we have an annotation or some kind of documentation on not using them anymore? Even deprecate them?

Currently it’s very hard to find the correct path for implementation. At least for me XD

As also the documentation is using such methods:

Yes, it’s in the settings.gradle file. But is that OK then?

1 Like

In the settings script that is very different, there you define the build and it is also not the actual project model that you modify there even if the method is named the same.

I have no idea why those APIs are not deprecated, maybe because if you use them, there is no “direct” replacement.
There are also quite some other APIs that are discouraged to be used but are not deprecated yet like the eager methods for which lazy pendants exist.
There might also some rare edge-cases where it is ok to use them, but in most cases you should not.

Actually, you can use them, it might just have bad consequences like not being able to use configuration cache or not being able to use isolated projects and so on.

To centralize build configuration, you should always prefer to use convention plugins, for example in buildSrc or - what I prefer - an included build, for example implemented as precompiled script plugins.

Besides not doing project coupling and thus working against the more sophisticated optimizations and features, this usually also makes the build setup clearer, better testable, and better maintainable.

Thanks for the insights.

I do use quite some binary plugins already , because I prefer small and compact build scripts.

Also, if I have to write something twice, I move it to some common place, like a task class or a plugin

Just DRY ^^

However, the root project has some helper tasks like the above, that just did not make sense to put somewhere else.

I am considering your solution with the separate task per project and joining the output later.

1 Like