Why would gradle correctly get the current date on one machine, but not on the other?

I have the War tasks configured like this to include the build date in the manifest file:

tasks.withType<War>().configureEach {
    val utcTimeZone = TimeZone.getTimeZone("UTC")
    val utcDateFormat = SimpleDateFormat("yyyy-MM-dd'T'00:00:00").apply { timeZone = utcTimeZone }
    manifest {
        attributes(
            mapOf(
            ...
                "Built-On" to utcDateFormat.format(Date())
            ...
            )
        )
    }
}

Originally the full date/time was included, which meant that every time the task ran the war file was rebuilt, even though nothing else changed. So, the compromise was made to only include the date without the time and then the war file would only get rebuilt once a day (assuming no changes to the app).

Similarly, I have the current date included in the file name for packaging the application into a zip file, like this:

tasks.register<Zip>("buildWindowsZip") {
    archiveClassifier = "Windows-" + SimpleDateFormat("yyyy-MM-dd").format(Date())
}

This seems to work fine on my machine, but on my co-worker’s machine sometimes the dates are not getting updated when he builds and packages the application, so he has to adjust them manually.
So, my question is that what could possibly cause this, and what steps can I take to diagnose and fix the issue? Also, is there a better, more reliable way to get the current date, so that Gradle recognizes the daily change while still utilizing the cache?
Thanks.

Just a guess, do you use configuration caching?

I don’t know how gradle should know that the current day is a changed input that will invalidate a cached configuration. When providing it as a project property, system property or environment variable that should work, but just from the JVM gradle probably does not know about that possibility and will simply restore a previously cached (serialized) configuration and not reevaluate the task configuration.

CC would also have been my first guess, as it should work fine without.
If it is CC, you need to use a ValueSource to calculate the date, so that a change invalidates the CC entry.

For example like this (I also replaced the Date usage - which you should never use nowadays - by modern date API) :

abstract class BuiltOnValueSource : ValueSource<String, ValueSourceParameters.None> {
    override fun obtain(): String = ZonedDateTime
        .now(ZoneId.of("UTC"))
        .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'00:00:00"))
}

tasks.withType<War>().configureEach {
    manifest {
        attributes(
            mapOf(
                "Built-On" to providers.of(BuiltOnValueSource::class) { }.get()
            )
        )
    }
}

Sorry, maybe I should have mentioned it, but yes, both of you are correct, I have configuration caching enabled (parallel caching too). I also agree that the current implementation was not good. However, part of my confusion is that the same project behaves differently on different computers.

Just to give a little more background, I have my convention plugins in the gradle/plugins directory as a separate project and have that included in my main project via the settings.gradle.kts:

pluginManagement {
    repositories.gradlePluginPortal()
    includeBuild("gradle/plugins")
}

When I execute the task on the main app project to package the application, it starts by executing the tasks in the plugins project:

❯ ./gradlew :app:bWZ
Parallel Configuration Cache is an incubating feature.
Calculating task graph as no cached configuration is available for tasks: :app:bWZ
> Task :plugins:checkKotlinGradlePluginConfigurationErrors SKIPPED
> Task :plugins:generateExternalPluginSpecBuilders UP-TO-DATE
> Task :plugins:extractPrecompiledScriptPluginPlugins UP-TO-DATE
> Task :plugins:compilePluginsBlocks UP-TO-DATE
> Task :plugins:generatePrecompiledScriptPluginAccessors UP-TO-DATE
> Task :plugins:generateScriptPluginAdapters UP-TO-DATE
> Task :plugins:compileKotlin UP-TO-DATE
> Task :plugins:compileJava NO-SOURCE
> Task :plugins:pluginDescriptors UP-TO-DATE
> Task :plugins:processResources UP-TO-DATE
> Task :plugins:classes UP-TO-DATE
> Task :plugins:jar UP-TO-DATE
...

And at the end I get this:

Configuration cache entry discarded because incompatible task was found: 'task `:app:buildWindowsZip` of type `org.gradle.api.tasks.bundling.Zip`'.

I checked the configuration-cache-report and found this explanation:

task `:app:buildWindowsZip` of type `org.gradle.api.tasks.bundling.Zip` is incompatible with the configuration cache. Reason: Archive classifier uses current calendar date.

If I re-run the same task again I get the same output. So far this would make sense that even though I am using the CC, Gradle found that my task is incompatible, so it is bypassing the cache and keeps re-evaluating the configuration every time. It is not efficient, but it gets the job done in that the date stays current. I have the same project cloned into 3 different directories and they all behaved this way…

Until last night, when I was poking around in one of these projects and suddenly Gradle switched to the behavior that I noticed on the other computer.

So, now the beginning of the output looks like this:

❯ ./gradlew :app:bWZ
Parallel Configuration Cache is an incubating feature.
Reusing configuration cache.
...

It says that it is reusing the cache and none of the above plugin tasks get executed. And at the end I get this message:

Configuration cache entry reused.

And as expected, when I executed the task today I got yesterday’s date. So, now what do we say which one is the ‘correct’ behavior and why is the other one different?

I don’t think that I can recreate this on demand, but in case it is worth looking into this any further I have one project that behaves one way, another the other way, and in the third one I implemented @Vampire’s suggestion. Thanks for that. I appreciate it.

If I re-run the same task again I get the same output. So far this would make sense that even though I am using the CC, Gradle found that my task is incompatible, so it is bypassing the cache and keeps re-evaluating the configuration every time. It is not efficient, but it gets the job done in that the date stays current. I have the same project cloned into 3 different directories and they all behaved this way…

Gradle does not have any detection like that I’m aware of.
I also did not find that message through Google or anywhere in the Gradle sources.
This behavior is only there if you define

notCompatibleWithConfigurationCache("Archive classifier uses current calendar date")

for that task.

Until last night, when I was poking around in one of these projects and suddenly Gradle switched to the behavior that I noticed on the other computer.

If this notCompatibleWithConfigurationCache is done in the build script, it should behave consistently across computers, so my guess here would have been, that you have an init script, for example in GRADLE_USER_HOME that does this notCompatibleWithConfigurationCache call only on your computer, which would explain that the same project behaves one way on your computer and different on your co-workers.

But if you say now that you have one project where it works and one where it does not on your computer, then this might also be unlikely.

Unless you have a condition in your init script that sometimes does that call and sometimes does not. Maybe check whether you have init scripts in your GRADLE_USER_HOME that causes this behaviour on your computer.

If you don’t find any, you should put a breakpoint inside the notCompatibleWithConfigurationCache call and then check with the debugger where this call is coming from for the project where CC is not reused.

It says that it is reusing the cache and none of the above plugin tasks get executed. And at the end I get this message:

This behavior is normal, the included build for build logic is part of the things the configuration cache can avoid if nothing changed.

So, now what do we say which one is the ‘correct’ behavior and why is the other one different?

That the CC is reused and uses the stale date is the correct behaviour if you do not manually say the task is incompatible (which should always just be a temporary measure until properly fixing it), or when you use a value source so that the calculated date is explicitly declared to be a CC input like I have shown.

For the archive classifier as it is a lazy property, the correct form would be

abstract class ArchiveClassifierWithDateValueSource : ValueSource<String, ValueSourceParameters.None> {
    override fun obtain(): String = "Windows-${
        ZonedDateTime
            .now(ZoneId.of("UTC"))
            .format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
    }"
}

tasks.register<Zip>("buildWindowsZip") {
    archiveClassifier = providers.of(ArchiveClassifierWithDateValueSource::class) { }
    from(layout.buildDirectory.dir("test-results"))
}

without the explicit get().

Thinking about it, even for the manifest the get() was majorly wrong.

It should be

abstract class BuiltOnValueSource : ValueSource<String, ValueSourceParameters.None> {
    override fun obtain(): String = ZonedDateTime
        .now(ZoneId.of("UTC"))
        .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'00:00:00"))
}

tasks.withType<War>().configureEach {
    manifest {
        attributes(
            mapOf(
                "Built-On" to providers.of(BuiltOnValueSource::class) { }
            )
        )
    }
}

because the manifest attribute values are even treated more lazy and only resolve at execution time.
So with this variant CC entry can be reused while still getting the current date written to the WAR.

In case of the archive classifier it is evaluated a bit earlier and thus does invalidate the CC entry, but Gradle then can decide properly.

I even would have thought for the manifest value part this should work as the value is only needed at execution time, but unfortunately, the provider seems to not be recognized as having a task dependency which usually causes it to not be stored to CC entry but evaluated at execution time:

tasks.withType<War>().configureEach {
    manifest {
        attributes(
            mapOf(
                "Built-On" to tasks.help.map {
                    ZonedDateTime
                        .now(ZoneId.of("UTC"))
                        .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'00:00:00"))
                }
            )
        )
    }
}

This should track the issue with the task-dependency-bearing provider not delaying evaluation: Jar task looses task dependency information for manifest attributes · Issue #26577 · gradle/gradle · GitHub

yyyy-MM-dd'T'HH:mm:ss and yyyy-MM-dd'T'HH_mm_ss, produce a new value on every invocation. Wired through a ValueSource means the CC entry invalidates every build, and the WAR is rebuilt every build.

So if daily granularity is still what you want, keep the zeros:

abstract class BuiltOnValueSource : ValueSource<String, ValueSourceParameters.None> {
    override fun obtain(): String = ZonedDateTime
        .now(ZoneId.of("UTC"))
        .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'00:00:00"))
}

The ValueSource approach from @Vampire is the right fix. It makes the date a declared CC input.

On the mystery of the differing machines, it’s definitely coming from your side. Try grepping for notCompatibleWithConfigurationCache across your build and any init scripts in your GRADLE_USER_HOME.

Yeah, I should have checked that. :man_facepalming: This is the first time I even looked at this report and I didn’t realize that reports can show customized messages.
I don’t have any init scripts; for one I didn’t think I needed that for anything, and also that would really turn into a “it works on my computer, but it doesn’t work of yours” type of thing, and I am trying to avoid any such scenarios.

I feel that using notCompatibleWithConfigurationCache is like saying, “I don’t know what I’m doing”. Which would be true in my case, but I’m still trying to be CC-compatible, even if I don’t always succeed.

So, like I mentioned, I have the same project cloned 3 times, and each one is on a different git branch (let’s say branch1, 2, and 3). In branch3 my coworker tried to resolve the stale date issue, by using AI. :face_with_peeking_eye:

Here is what the change looked like:

val buildDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")

fun currentBuildDate(): String = LocalDate.now().format(buildDateFormatter)

tasks.withType<War>().configureEach {
    // Date metadata must reflect the build invocation day; disable config cache for this task.
    notCompatibleWithConfigurationCache("Manifest Built-On uses current calendar date")
    // Make the build date part of task inputs so a new local day invalidates stale manifest metadata.
    inputs.property("builtOnDate", providers.provider { currentBuildDate() })
    manifest {
        attributes(
            mapOf(
                "Built-On" to "${currentBuildDate()}T00:00:00"
            )
        )
    }

    doFirst {
        manifest.attributes(mapOf("Built-On" to "${currentBuildDate()}T00:00:00"))
    }
}

tasks.register<Zip>("buildWindowsZip") {
    // Date-stamped archive names cannot be restored from configuration cache across days.
    notCompatibleWithConfigurationCache("Archive classifier uses current calendar date")
    val archiveDateProvider = providers.provider { currentBuildDate() }
    archiveClassifier.set(archiveDateProvider.map { "Windows-$it" })
    inputs.property("archiveDate", archiveDateProvider)
    ...    
}

I think this would scare away even a Vampire, better than garlic. :man_vampire: So, you were right, this is where the message came from. But I told him that we need to revert this thing right away and find a real solution.
Searching for notCompatibleWithConfigurationCache gave me too many hits that were irrelevant, so instead I searched for the actual message and I found 3 files in 3 different directories that contained the string:

$GRADLE_USER_HOME/caches/9.7.1/transforms/<key>/transformed/instrumented/instrumented-plugins.jar

Inside the jar file the message was in a class file with the same name as my plugin script. So, this all starting to make a little more sense, but now my question is, are these cache entries shared across all projects on a computer? I thought that the CC was per project, but this is the only way I could explain how a change in one branch in one project would affect another project that is also on a different branch.

Yes, thank you, I also noticed that and adjusted it, like you suggested. But actually I am trying to get it changed, so that we don’t even have the time, only the date, because it doesn’t make much sense.

Yes, it seems that way. But it is still strange how apparently a change in one project affected the others.

yyyy-MM-dd'T'HH:mm:ss and yyyy-MM-dd'T'HH_mm_ss, produce a new value on every invocation. Wired through a ValueSource means the CC entry invalidates every build, and the WAR is rebuilt every build.

So if daily granularity is still what you want, keep the zeros:

Yes, of course.
I just inserted the time to test properly and forgot to replace it back before posting the examples.
Fixed my code examples.

The ValueSource approach from @Vampire is the right fix. It makes the date a declared CC input.

Actually imho the task-dependency-bearing-provider would be the correct fix, if the Gradle bug were fixed, because there should be no need to invalidate the CC entry. But as long as the bug is not fixed, the value source should be the way to go, yes.

I don’t have any init scripts; for one I didn’t think I needed that for anything, and also that would really turn into a “it works on my computer, but it doesn’t work of yours” type of thing, and I am trying to avoid any such scenarios.

Yeah, definitely.
There are valid use-cases for init scripts, but they should optimally never influence the actual build outcome.

I feel that using notCompatibleWithConfigurationCache is like saying, “I don’t know what I’m doing”. Which would be true in my case, but I’m still trying to be CC-compatible, even if I don’t always succeed.

Not really.
It says “this task is currently not yet compatible with CC, so disable CC if this task is part of the task graph”.
This should - as mentioned - always just be a temporary measure until the task was made CC compatible.
If there is a way to make it CC compatible like here using a value source, this is the better way.
But it could for example be some task from some third-party plugin where the implementation is not yet CC compatible and you need to mark it so until the plugin was updated or replaced by a CC-compatible alternative.

So, like I mentioned, I have the same project cloned 3 times, and each one is on a different git branch (let’s say branch1, 2, and 3).

You did not say that actually. You just said “I have the same project cloned into 3 different directories and they all behaved this way…”, not that you checked out different branches. If it is different branches, this explains why they behave differently if they have different build scripts like you mentioned now.

Here is what the change looked like:

Like many AI slop, this has quite some problems.
Without the notCompatibleWithConfigurationCache it would fail, because the code tries to access the currentBuildDate and buildDateFormatter top-level declarations of the build script which would mean it would need to serialize the build script instance to the CC entry which is not supported.

Besides that changing the configuration of a task during execution phase like the doFirst in the War configuration does is almost never a good idea and almost always to be avoided. Especially if the changed configuration changes the inputs or outputs like it does here which would prevent up-to-date checks (or for cacheable tasks the task output cache) to work properly and thus result in stale results being produced and / or used.

But I told him that we need to revert this thing right away and find a real solution.

:ok_hand:

Searching for notCompatibleWithConfigurationCache gave me too many hits that were irrelevant

You should fix practically all of them. As I said, they are only temporary band-aid work-arounds, not proper solutions. :slight_smile:

are these cache entries shared across all projects on a computer? I thought that the CC was per project

Theoretically, I think yes.
Practically, usually not, as it is very unlikely that the CC inputs for different projects are identical, so the CC entry key would usually be different.

how a change in one branch in one project would affect another project that is also on a different branch.

Different branches of the same project can well reuse the same CC entries if the build logic is identical and thus the CC inputs could be identical, so it would be a waste of time not to reuse it.
That this happens cross-project is quite unlikely but theoretically possible.

I’m sure, and maybe at some point, once I gain more knowledge and experience, I might look into that, but for now I don’t want to further complicate things.

Yes, of course, I understand that there are legitimate use-cases for this setting. I was just trying to make fun of my own incompetence, but I need to get more serious. But in all seriousness, in this case it is actually true what I said, because that AI “solution” wasn’t just a one time deal, but an iterative approach over 3 different attempts. From the comments in the code it seems that at first AI was trying to do the “right thing” by creating some kind of input into the task, just it didn’t implement it correctly. So, when those changes didn’t work, it ran out of ideas and decided that the only option is to make the task non-CC compatible. But hopefully an AI agent will read this thread, see your example, and learn from it. :grin:

Yes, you’re right, initially I didn’t mention that there are different branches, because I wasn’t aware of this change, so I thought that all three branches were running the same Gradle code. And because of that my initial assumptions were also wrong.
But still, the fact remains that the change was made only in one project/branch, and that apparently affected other projects/branches on the same machine. Which you say is unlikely, but possible, since the cache is shared across all projects on the machine.
And then simply by making some temporary changes, refreshing Gradle, running some tasks, and then eventually reverting those changes, I was able to alter the behavior of the execution of these affected projects, so that they no longer used that shared cache entry.

The reason I said that they are irrelevant is because they are part of the Gradle code or some plugins. We don’t use notCompatibleWithConfigurationCache in any of our scripts. Well, except in this one case it slipped in.

From the comments in the code it seems that at first AI was trying to do the “right thing” by creating some kind of input into the task, just it didn’t implement it correctly.

No, not really.
The manifest values are automatically build inputs.
So an additional task input does not change anything.
The problem is, that it was not a configuration-cache input, that’s something completely different.

And then simply by making some temporary changes, refreshing Gradle, running some tasks, and then eventually reverting those changes, I was able to alter the behavior of the execution of these affected projects, so that they no longer used that shared cache entry.

That doesn’t sound right.
The theoretic case where it could be reused is, if the CC inputs are all the same and that is near to impossible for different projects.
And if it would be reused, then it should be correct to reuse.
Doing a change and reverting that change should then also not “fix” it but still reuse that CC entry.
If you can reproduce that behavior somehow, you should open a bug report about it.

The reason I said that they are irrelevant is because they are part of the Gradle code or some plugins. We don’t use notCompatibleWithConfigurationCache in any of our scripts

:ok_hand:

I doubt that I will be able to recreate it, even if I can remember what I did. But I’ll think about it. Thanks for your help.