Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build finished event now deprecated #20151

Closed
baron1405 opened this issue Mar 11, 2022 · 30 comments
Closed

Build finished event now deprecated #20151

baron1405 opened this issue Mar 11, 2022 · 30 comments
Assignees
Labels
Milestone

Comments

@baron1405
Copy link
Contributor

Starting in Gradle 7.4, both the Gradle.buildFinished and BuildListener.buildFinished are deprecated. Apparently, it was not possible to get these methods to work with the configuration caching feature. The deprecation message does not list an alternative mechanism for being called when a build completes. In addition to this deprecation breaking one of my plugins, it seems like a serious regression to remove the ability to receive such a fundamental event as the completion of the build. Perhaps I am missing some other mechanism for receiving this event.

Expected Behavior

Ability to register an event handler for build completion.

Current Behavior

The buildFinished event has been deprecated with no apparent replacement.

Context

Deprecation in Gradle 7.4.

@baron1405 baron1405 added a:regression This used to work to-triage labels Mar 11, 2022
@jbartok jbartok added in:lifecycle-tasks work with or wire up lifecycle tasks in:configuration-cache Configuration Caching and removed in:lifecycle-tasks work with or wire up lifecycle tasks labels Mar 11, 2022
@jbartok jbartok removed the to-triage label Mar 11, 2022
@ljacomet
Copy link
Member

Indeed, these are deprecated and planned to be removed.

Alternatives have been documented but I can see that the Javadoc does not point to that information.

Is your build emitting a deprecation message? Or are you seeing the deprecation only in the context of the IDE when editing the script?

@ljacomet
Copy link
Member

Gradle 7.5 will have more information on this deprecation and the alternatives that are configuration cache compatible.

@ljacomet ljacomet removed their assignment Mar 11, 2022
@baron1405
Copy link
Contributor Author

Thanks @ljacomet for the quick reply! I have my build treat deprecations as errors so that is why I see it. The IDE also shows the method as deprecated. Thanks for the pointer to the docs. I missed this while looking through the release notes. It will definitely help others to have a pointer to the alternatives in the Javadoc.

@eskatos eskatos added @configuration-cache and removed in:configuration-cache Configuration Caching labels Mar 16, 2022
@eskatos eskatos added this to the 7.5 RC1 milestone Mar 16, 2022
@eskatos eskatos removed this from the 7.5 RC1 milestone Apr 15, 2022
@thumannw
Copy link

@ljacomet The current docs (release notes) point to the Build Service feature with no further information. I checked the docs for 7.5 RC and nightly but did not find anything new regarding buildFinished. Will there be other alternatives in 7.5 as you mentioned above?

@bernhardkern
Copy link

@ljacomet: Still I do not get the documentation, even with the link you provided. Your link points to Build finished events -> configuration cache chapter -> build services.

Does it mean we have to implement a custom build service to solve the issue?

@eskatos
Copy link
Member

eskatos commented Jun 2, 2022

The deprecation in 7.4 is soft: only the @Deprecated javadoc annotation was added, no deprecation warning logged. It will be completely deprecated once the alternative is implemented. This should come shortly but won't be in 7.5.

In the context of using the configuration cache you can use an AutoCloseable build service as an approximation of the end of the build. You can't get the BuildResult there though. This will be possible in some form with the upcoming alternative.

cc @gradle/bt-configuration-cache

@baron1405
Copy link
Contributor Author

@eskatos and @ljacomet Thanks guys for following up on this issue. In general, I highly recommend that Gradle does not deprecate (strongly or softly) any major feature such as the build status callbacks without prior warning and a reasonable replacement that is ready for use at the time of the deprecation (not at the time of removal).

@aSemy
Copy link
Contributor

aSemy commented Aug 5, 2022

Indeed, these are deprecated and planned to be removed.

Alternatives have been documented but I can see that the Javadoc does not point to that information.

Is your build emitting a deprecation message? Or are you seeing the deprecation only in the context of the IDE when editing the script?

Can you link directly to the documented alternatives? The link you provided isn't very clear. I eventually end up at the bottom of this page https://docs.gradle.org/current/userguide/build_services.html#operation_listener, but the description isn't clear.

A build service can be used to receive events as tasks are executed. To do this, create and register a build service that implements OperationCompletionListener. Then, you can use the methods on the BuildEventsListenerRegistry service to start receiving events.

What is that supposed to mean? What's a BuildEventsListenerRegistry? How do I use 'the methods'?

@aSemy
Copy link
Contributor

aSemy commented Aug 8, 2022

Okay I think I've figured it out. Here's a 'minimal' example you can copy into a build.gradle.kts.

// build.gradle.kts

abstract class BuildListenerService :
    BuildService<BuildListenerService.Params>,
    org.gradle.tooling.events.OperationCompletionListener {

    interface Params : BuildServiceParameters

    override fun onFinish(event: org.gradle.tooling.events.FinishEvent) {
        println("BuildListenerService got event $event")
    }
}

val buildServiceListener = gradle.sharedServices.registerIfAbsent("buildServiceListener", BuildListenerService::class.java) { }

abstract class Services @Inject constructor(
    val buildEventsListenerRegistry: BuildEventsListenerRegistry
)

val services = objects.newInstance(Services::class)

services.buildEventsListenerRegistry.onTaskCompletion(buildServiceListener)

In the logs I can see the printed message

> Task :service:vp:assemble UP-TO-DATE
> Task :assemble UP-TO-DATE
> Task :service:arc:processResources NO-SOURCE
> Task :service:ar:processResources UP-TO-DATE
> Task :service:ara:processResources UP-TO-DATE
BuildListenerService got event Task :service:vp:assemble UP-TO-DATE
BuildListenerService got event Task :assemble UP-TO-DATE
BuildListenerService got event Task :service:arc:processResources skipped
BuildListenerService got event Task :service:ar:processResources UP-TO-DATE
BuildListenerService got event Task :service:ara:processResources UP-TO-DATE
> Task :service:ti:kaptGenerateStubsKotlin UP-TO-DATE
BuildListenerService got event Task :service:ti:kaptGenerateStubsKotlin UP-TO-DATE
> Task :service:ac:kaptGenerateStubsKotlin UP-TO-DATE
BuildListenerService got event Task :service:ac:kaptGenerateStubsKotlin UP-TO-DATE
> Task :service:ti:kaptKotlin UP-TO-DATE
BuildListenerService got event Task :service:ti:kaptKotlin UP-TO-DATE
> Task :service:ti:compileKotlin NO-SOURCE
BuildListenerService got event Task :service:ti:compileKotlin skipped
> Task :service:ti:compileJava NO-SOURCE
BuildListenerService got event Task :service:ti:compileJava skipped
> Task :service:ti:processResources NO-SOURCE

@ingokegel
Copy link
Contributor

@aSemy Thank you!

I have tried this too, but BuildEventsListenerRegistry.onTaskCompletion is only for tasks, not for the entire build. There would have to be a method BuildEventsListenerRegistry.onProjectCompletion, then it would work.

@eskatos Can you add something like the above for Gradle 7.6 or explain if there is a different way to notify the build service that the entire build has completed?

@SimonCJacobs
Copy link

I just added this to my Kotlin script and it seems to be working ok. Pleased to hear of any improvements.

val buildEvents: BuildEventsListenerRegistry // obtain by injection into appropriate object, eg per @aSemy

project.gradle.taskGraph.whenReady {
   project.gradle.sharedServices.registerIfAbsent("buildFinished", BuildFinishedService::class.java) {
      parameters.lastTaskPath = project.gradle.taskGraph.allTasks.last().path
   }
      .let { buildEvents.onTaskCompletion(it) }
}

abstract class BuildFinishedService : BuildService<BuildFinishedService.Parameters>, OperationCompletionListener {
   interface Parameters : BuildServiceParameters {
      var lastTaskPath: String
   }

   override fun onFinish(event: FinishEvent?) {
      (event as TaskFinishEvent).let {
         if (event.descriptor.taskPath == parameters.lastTaskPath)
            buildFinishedAction()
      }
   }

   private fun buildFinishedAction() {
      // Do something exciting
   }
}

@maguro
Copy link

maguro commented Feb 2, 2023

Respectfully, what the heck? Speaking to those who thought that deprecating buildFinished() was a good idea, possibly, in favor of a BuildEventsListenerRegistry implementation: how can anyone think that this long winded boilerplate code is a good idea?

I'm hoping that BuildEventsListenerRegistry is not part of the master plan for this deprecation and, instead, is an honest community effort to get some kind of workaround in the face of the dearth of information being provided by the Gradle development team. :)

@bamboo
Copy link
Member

bamboo commented Feb 3, 2023

Hi everyone, this public specification describes how we're planning to improve the situation in 8.1.
Let us know what you think.

@bamboo
Copy link
Member

bamboo commented Mar 9, 2023

Implemented in #17659

@asarkar
Copy link

asarkar commented May 17, 2023

@bamboo Is there any documentation with examples showing how to use the new APIs to solve the problem this ticket was for?

@ingokegel
Copy link
Contributor

@asarkar

https://docs.gradle.org/current/userguide/dataflow_actions.html

It works, but the concepts you have to understand are quite a handful for such a simple task. I wonder if one could develop some sort of convenience method that is close to the original buildFinished listener.

@asarkar
Copy link

asarkar commented May 17, 2023

@ingokegel I've read that already, but it's so severely constricted it seems to be written for Twitter in mind. I know no more after reading it than I knew before.
https://docs.gradle.org/current/userguide/dataflow_actions.html#lifecycle_event_providers

@ingokegel
Copy link
Contributor

ingokegel commented May 17, 2023

@asarkar It is indeed very complicated. On the FlowScope that you get from the injected getFlowScope() method, you call always() (so it is "always" part of the task graph which is currently the only option) with a lambda that calls getFlowProviders().buildWorkResult.map{ /* configure FlowActionSpec */ } on the injected FlowProvider which runs your code in the FlowActionSpec lambda. There, you wire a Gradle Property field in your FlowParameters object with a provider that uses the action build result, possibly just a boolean.

buildWorkResult (currently the only available flow provider) becomes available when the build is finished. When all properties of the FlowAction become available, it is then executed automatically (this is somewhat strange and not explained in the documentation). Compared to the old buildFinished listener, it is a lot harder to access any other state and you have to do it with the FlowParameters of the FlowAction, using Gradle Property fields in the parameter object, while remembering that the flow action will only be triggered once all of the properties of the FlowParameters become available.

Looking at the code it is very difficult to understand what is happening and I really wonder if all of this is necessary just to work with the build cache.

@artsmvch
Copy link

artsmvch commented May 22, 2023

Hello!
I can track the end of builds using getFlowProviders().buildWorkResult.map{ /* configure FlowActionSpec */ } but is there any way to know when the build started? I want to calculate the entire build duration.

@JojOatXGME
Copy link

Hi, I was trying to replace the usage of buildFinished in the nix-idea plugin. The code checks if the build failed for a particular reason, and if so, provides some guidance for the user. Here is what I came up with for now. I have to say that I don't like that the parameters implicitly define when the action is executed. Having real lifecycle hooks would seem more intuitive and less error-prone to me. Is there a reason for modeling a new dynamic execution graph next to tasks, instead of simply exposing a method which allows adding BUILD_PHASE listeners next to BuildEventsListenerRegistry.onTaskCompletion?

Show potential replacement for code in nix-idea ...
import org.jetbrains.intellij.tasks.RunIdeBase

// I haven't found a way to access services (like FlowScope) from script plugins,
// so I created this nested plugin class.
apply<DelegatePlugin>()
abstract class DelegatePlugin : Plugin<Project> {
    @get:Inject
    protected abstract val flowScope: FlowScope

    @get:Inject
    protected abstract val flowProviders: FlowProviders

    override fun apply(target: Project) {
        val jbr = target.providers.of(JbrSource::class.java) {
            parameters.path.set(target.file("jbr/bin/java"))
        }.orNull

        if (jbr == null) {
            flowScope.always(JbrGuidance::class) {
                parameters.project.set(target)
                parameters.buildResult.set(flowProviders.buildWorkResult)
            }
        } else {
            target.tasks.withType<RunIdeBase> {
                projectExecutable.set(jbr)
            }
        }
    }
}

abstract class JbrSource : ValueSource<String?, JbrSource.Parameters> {
    interface Parameters : ValueSourceParameters {
        @get:Input
        val path: Property<File>
    }

    override fun obtain(): String? {
        return parameters.path.get().takeIf { it.exists() }?.toString()
    }
}

abstract class JbrGuidance : FlowAction<JbrGuidance.Parameters> {
    private val regex = Regex("""\.gradle/.*/jbr/.*/java\b""")

    interface Parameters : FlowParameters {
        @get:Input
        val project: Property<Project>

        @get:Input // Unused, but tells Gradle that this action shall be executed at the end of the build
        val buildResult: Property<BuildWorkResult>
    }

    override fun execute(parameters: Parameters) {
        //val logger = org.slf4j.LoggerFactory.getLogger("jbr-guidance")
        val project = parameters.project.get()
        val logger = project.logger
        val taskGraph = project.gradle.taskGraph
        for (task in taskGraph.allTasks) {
            if (task.project == project && task is RunIdeBase &&
                task.state.failure?.cause?.message?.contains(regex) == true
            ) {
                logger.error("""
                    |
                    |! Info for users on NixOS:
                    |!
                    |! The JetBrains Runtime (JBR) downloaded by Gradle is not compatible with NixOS.
                    |! You may run the ‘:jbr’ task to configure the runtime of <nixpkgs> instead.
                    |! Alternatively, you may run the following command within the project directory.
                    |!
                    |!   nix-build '<nixpkgs>' -A jetbrains.jdk -o jbr
                    |!
                    |! This will create a symlink to the package jetbrains.jdk of nixpkgs at
                    |! ${'$'}projectDir/jbr, which is automatically detected by future builds.
                    """.trimMargin())
                break
            }
        }
    }
}

@samoylenkodmitry
Copy link

samoylenkodmitry commented Jul 11, 2023

Is there any example project showing how to replace

	gradle.buildFinished {
...
	}

to the working solution with all project dirs/paths and files that are needed provided?

@TWiStErRob
Copy link
Contributor

TWiStErRob commented Jul 11, 2023

@samoylenkodmitry there's a working example in the Gradle repo which is used to generate the docs.


Suggestion: You don't necessarily need the full ceremony of a Settings plugin, you can copy the body of apply (along with the two other classes) into settings.gradle.kts, and get the two injected fields imperatively via serviceOf<T>() in Kotlin DSL. See TWiStErRob/repros@97880ba

TWiStErRob added a commit to TWiStErRob/repros that referenced this issue Jul 20, 2023
@Fermiz
Copy link

Fermiz commented Oct 8, 2023

Thanks to @SimonCJacobs's demo and serviceOf() in Kotlin DSL mentioned by @TWiStErRob, I made it after half a day's spike. To save time, here is my full code snippets to print the time-consuming statistics of each task:

import org.gradle.tooling.events.OperationCompletionListener
import org.gradle.tooling.events.FinishEvent
import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.tooling.events.OperationResult
import org.gradle.tooling.events.FailureResult

abstract class StatisticsService : BuildService<StatisticsService.Parameters>,
    OperationCompletionListener {

    interface Parameters : BuildServiceParameters {
        var lastTask: String
        var timeStatistics: MutableMap<String, Long>
    }

    override fun onFinish(event: FinishEvent) {
        val taskName = event.descriptor.name
        val operationResult = event.result
        val durationSeconds = TimeUnit.MILLISECONDS.toSeconds(operationResult.endTime - operationResult.startTime)

        parameters.timeStatistics.putIfAbsent(taskName, durationSeconds)
        println("[$taskName] time elapsed: ${durationSeconds}s")

        if (operationResult is FailureResult || taskName == parameters.lastTask) {
            println("//// Task Time Statistics ////")
            parameters.timeStatistics.toList().sortedByDescending { (_, duration) -> duration }.toMap()
               .forEach { (task, duration) -> println("[${task.padEnd(32)}] : ${duration}s") }
        }
    }
}

gradle.taskGraph.whenReady {
    val lastTask = gradle.taskGraph.allTasks.last().path

    val timeStatistics: MutableMap<String, Long> = mutableMapOf()

    val demoListener = gradle.sharedServices.registerIfAbsent(
        "demoListener ", StatisticsService::class.java
    ) {
        parameters.lastTask = lastTask
        parameters.timeStatistics = timeStatistics
    }

    gradle.serviceOf<BuildEventsListenerRegistry>().onTaskCompletion(demoListener)
}

Hope it would be useful for the new beginers who is looking for the gradle hooks alternative on the latest version.

@TWiStErRob
Copy link
Contributor

TWiStErRob commented Oct 8, 2023

Sorry for the off-topic, but would like to point out some potential pitfalls related to @Fermiz's code. I recommend continuing discussion in Gradle Community Slack.

For "build-end" detection it's probably better to use the FlowScope.always event, even if it's a bit more code, because the way the "last task" is used above may not always be correct.

For example in AGP

We have assembleDebug and assembleRelease, both building the same-ish thing in --parallel, but assembleRelease contains a minification step, which makes it overall longer. If the command is gradlew assembleRelease assembleDebug, the last command is likely assembleDebug, but it's very likely minifyReleaseWithR8 (very long task) and therefore assembleRelease (which depends on R8) will complete later than than the "last task" picked from the graph.


Also with --continue one can have multiple failing tasks in the same build.

For example

In a multi-module project, detekt (or similar static analysis tool) can run and fail on each module resulting in summary statistics printed many times.


Both of these could result in mis-information from the stats, missing long tasks from the table; or if the code is not printing, duplicate processing of potentially wrong data.

@JojOatXGME
Copy link

JojOatXGME commented Mar 17, 2024

@Fermiz, your BuildService can just implement AutoCloseable to detect the end of the build.

A build service implementation can also optionally implement AutoCloseable, in which case Gradle will call the build service instance’s close() method when it discards the service instance. This happens some time between completion of the last task that uses the build service and the end of the build.
https://docs.gradle.org/current/userguide/build_services.html#implementing_a_build_service

The only limitation compared to the FlowAction is that you will not get the overall build result. However, you can get the result of each individual task, as you are already doing in your StatisticsService. Here is an example where I used the combination of BuildEventsListenerRegistry.onTaskCompletion and AutoCloseable to replace gradle.buildFinished.

https://github.com/NixOS/nix-idea/blob/f25cb9b9714bfd05fc71c8490ea4f4f84ee72e79/gradle/plugins/src/main/kotlin/local.jbr-guidance.gradle.kts#L70-L88

Note that a build may still fail, even if there is not a single failing task. For example, the serialization of task inputs may fail when the configuration cache is enabled. I don't know why Gradle doesn't just provide a method similar to BuildEventsListenerRegistry.onTaskCompletion for build completion. The FlowAction is unnecessary complex and counterintuitive in my opinion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Development

No branches or pull requests