4-1: Gradle Lifecycle Hooks β
Picking up from our previous sharing in Section 2-2 on lifecycles, we're going to delve into the Hook mechanism provided by Gradle. Hook functions allow developers to tap into specific points of the build process. It's vital to note that these hooks operate at a global level and are not merely about appending logic to a single Task or Action.
You'll find the sample code for this section in the gradle-lifecycle project. We won't be reproducing complete hook codes here due to the fragmented nature of the their invocations. However, to see them in action, execute ./gradlew clean -I init.gradle.kts
. This will allow you to monitor the output and witness the timing of hook usage in relation to where the lifecycle class from build-logic is invoked. For clarity, please revisit the overview diagram of Gradle Lifecycle in Section 2-2 (Figure 2.2.1).
4-1-1: Categorizing the Hooks β
The realm of Gradle's lifecycle hooks is vast, and there's no official classification. I've taken the liberty to categorize them into three groups based on the caller types and accepted parameters for each hook API.
For Context Objects with Action β
First up, we have hooks that can be directly invoked through key context objects. Examples include Gradle#afterProject(...)
, Project#afterEvaluate(...)
, and TaskGraph#beforeTask(...)
. The overview provided above is primarily based on these hooks. Characteristically, these hooks accept Action
(or Closure
) as parameters. This means you can instantly pass a closure to monitor outcomes. Although they're straightforward to use, their APIs are a bit scattered, requiring interaction with multiple context objects.
gradle.taskGraph.whenReady {
Lifecycle.whenTaskGraphIsReady("whenReady")
}
...
project.beforeEvaluate {
Lifecycle.beforeEvaluate(this.displayName)
}
...
tasks.whenTaskAdded {
Lifecycle.whenTaskAdded(proj.name + ":" + this.name)
}
The forthcoming segments will shed more light on the utility and functions of each hook.
Listener Callbacks β
Listeners provide a powerful mechanism to tap into various stages of the build process. Here, we'll focus on two primary methods to add listeners and the range of available listeners they accommodate.
Method to Add Listeners | Listeners Offered |
---|---|
Gradle#addListener(...) | 1. TaskExecutionGraphListener: graphPopulated 2. TaskExecutionListener: beforeExecute, afterExecute 3. TaskExecutionAdapter: beforeExecute, afterExecute 4. TaskActionListener: beforeActions, afterActions |
Gradle#addBuildListener(...) | BuildListener: settingsEvaluated, projectsLoaded, projectsEvaluated, buildFinished |
The name and function of most listeners mirror those of the first category (Context Object with Action) . The process of configuring other listeners follows a familiar pattern. For more practical implementations, we'll delve deeper into hook usage scenarios later on.
gradle.addBuildListener(object : BuildListener {
override fun settingsEvaluated(settings: Settings) {
Lifecycle.onSettingsEvaluated(...)
}
override fun projectsLoaded(gradle: Gradle) {
Lifecycle.onProjectsLoaded(...)
}
override fun projectsEvaluated(gradle: Gradle) {
Lifecycle.onProjectsEvaluated(...)
}
override fun buildFinished(result: BuildResult) {
Lifecycle.onBuildFinished(...)
}
})
Contrasting the two API types, the second offers a broader perspective. It grants callback parameters that present context object information relevant to that stage. However, it doesn't offer the same granularity as the first type. These API types can be seen as complementary.
Event Notifications β
The third category brings us to event notifications. These are similar in essence to listeners, designed to alert of various events during the build process. In light of the Configuration Cache (refer to Section 4-5), several APIs from both the first and second types, such as taskGraph.beforeTask
and gradle.buildFinished()
during the task execution phase (Execution), have been labeled as incompatible. Some have received the @Deprecated
tag. This newly introduced event notification API emerges as a potential substitute for the now-deprecated ones.
To register for these event notifications, employ the BuildEventsListenerRegistry
. Then implementing the OperationCompletionListener
provides the listening mechanism. As of now, the BuildEventsListenerRegistry
offers a singular API, onTaskCompletion(Provider<? extends OperationCompletionListener> listener)
, dedicated to notifying when a task completes. Gradle's documentation nudges us towards the Shared Build Services for creating these listeners. For a more in-depth look into this, head over to Section 4-4.
// Leveraging the Gradle's built-in dependency injection
// to fetch the `BuildEventsListenerRegistry`.
class BuildEventsListenerRegistryPlugin @Inject constructor(
private val registry: BuildEventsListenerRegistry
) : Plugin<Settings> {
override fun apply(settings: Settings) {
settings.gradle.run {
val buildFinishTrackService = sharedServices.registerIfAbsent(
"buildFinishTrackService", BuildFinishTrackService::class) {
parameters {}
}
registry.onTaskCompletion(buildFinishTrackService)
}
}
}
abstract class BuildFinishTrackService : BuildService<Parameters>,
OperationCompletionListener {
...
override fun onFinish(e: FinishEvent) {
if (e is TaskProgressEvent) {
when (e) {
is TaskStartEvent -> {
Lifecycle.afterTask("TaskStartEvent")
}
is TaskFinishEvent -> {
Lifecycle.afterTask("TaskFinishEvent")
when (val res = e.result) {
is SuccessResult -> {
Lifecycle.afterTask(
"${e::class.simpleName} >>> ${res::class.simpleName} "
+ ">>> Res: Success. >>> EndTime: ${res.endTime}."
)
val desc: TaskOperationDescriptor = e.descriptor
Lifecycle.afterTask("taskPath: ${desc.taskPath}")
// Lifecycle.afterTask(desc.dependencies.first().displayName)
// Lifecycle.afterTask("${desc.taskPath} belongs to " +
// "${desc.originPlugin ?: "Unknown"}")
}
is SkippedResult -> {
Lifecycle.afterTask(...)
}
is FailureResult -> {
Lifecycle.afterTask(...)
}
}
}
...
}
}
}
...
}
Gaining insights from a task event primarily revolves around the attribute fields present within the TaskOperationDescriptor
. Yet, beyond the timing metrics, only the taskPath
is practically useful. Even though fields like dependencies
and originPlugin
have public
visibility and the documentation mentions their availability since Gradle 5.1, invoking them via external Plugins will unfortunately throw an UnsupportedMethodException
.
Additionally, the taskPath
provides a comprehensive task name, e.g., :lib:preDebugBuild
. To adeptly parse this, a glimpse into AGP's monitoring code can be helpful. Let's look at a few crucial functions derived from there:
// For a full view, check the following classes:
// com.android.build.gradle.internal.profile.AnalyticsService
// com.android.build.gradle.internal.profile.AnalyticsResourceManager
fun recordTaskExecutionSpan(finishEvent: FinishEvent?) {
if (finishEvent == null || finishEvent !is TaskFinishEvent) return
val taskPath = finishEvent.descriptor.taskPath
val taskRecord = getTaskRecord(taskPath) ?: return
val typeName = getTypeName(taskPath) ?: return
val taskType = getTaskExecutionType(typeName)
val taskResult = finishEvent.result
...
}
fun getTaskRecord(taskPath: String) : TaskProfilingRecord? {
if (!taskRecords.containsKey(taskPath)) {
...
val projectPath = getProjectPath(taskPath) ?: return null
val variantName = getVariantName(taskPath)
taskRecords[taskPath] = ...
}
return taskRecords[taskPath] // [I]
}
fun collectTaskMetadata(graph: TaskExecutionGraph) {
graph.allTasks.forEach { task ->
val variantName = if (task is VariantAwareTask) {
task.variantName
} else {
task.extensions.findByName(PROPERTY_VARIANT_NAME_KEY) as String?
}
taskMetadata[task.path] = TaskMetadata(
task.project.path,
variantName,
AnalyticsUtil.getPotentialTaskExecutionTypeName(task.javaClass))
}
}
private fun getProjectPath(taskPath: String) : String? =
taskMetadata[taskPath]?.projectPath
private fun getVariantName(taskPath: String) : String? =
taskMetadata[taskPath]?.variantName
private fun getTypeName(taskPath: String) : String? =
taskMetadata[taskPath]?.typeName
This offers a good look into the role of the OperationCompletionListener
in task analysis. For example, we can find a detailed record of the Task with getTaskRecord(...)
(at [I]).
What's more, setting up a BuildService
isn't strictly required. When we avoid handling Task
listening in Settings
and move it to Project
, the BuildEventsListenerRegistry
instance can be retrieved through the inherent ObjectFactory
by Gradle's Managed Classes/Properties features.
abstract class Listener : OperationCompletionListener {
override fun onFinish(event: FinishEvent) {
println("onTaskFinish: $event")
}
}
interface RegistryProvider {
@get:Inject
val registry: BuildEventsListenerRegistry
}
objects.newInstance<RegistryProvider>().registry
.onTaskCompletion(provider { objects.newInstance<Listener>() })
Beyond the Task events, Gradle has laid the foundation for a myriad of other notification events, nestled under the org.gradle.tooling.events
package.
import org.gradle.tooling.events.configuration.ProjectConfigurationProgressEvent
import org.gradle.tooling.events.download.FileDownloadProgressEvent
import org.gradle.tooling.events.task.TaskProgressEvent
import org.gradle.tooling.events.test.TestProgressEvent
import org.gradle.tooling.events.transform.TransformProgressEvent
import org.gradle.tooling.events.work.WorkItemProgressEvent
From the above, besides the TaskProgressEvent
, we can spot five more public XxxProgressEvent
classes, all of which have a structure akin to the task event. Although, there isn't currently an entry point that supports their reception, you may notice these events are part of the Gradle Tooling API, sharing the same foundational architecture as the Gradle Scan Plugin probably. So, it's worth keeping an ear to the ground as we might witness more expansive event support in upcoming Gradle versions.
While the event notification API can replicate certain functionalities, especially the monitoring capability of the second API set when used in afterTask
, it's pivotal to remember that the gradle.buildFinished()
of the first API is deprecated. However, there's a nifty workaround:
abstract class BuildFinishTrackService : BuildService<Parameters>,
OperationCompletionListener, AutoCloseable {
...
override fun close() {
Lifecycle.onBuildFinished("Triggered by AutoCloseable.")
}
}
We've adapted the BuildFinishTrackService
by incorporating the AutoCloseable
interface and, subsequently, the close()
method. If you delve into the documentation, it explains that the BuildService
feature brings the service to a close at the right moment through the JDK's AutoCloseable
interface. This is typically sandwiched between the completion of relevant tasks and the end of the build. Given the registration setup of OperationCompletionListener
, our service is activated without fail and persists until the final task wraps up. Therefore, this provides a seamless alternative to the buildFinished()
method.
4-1-2: Hooks in the Initialization Phase β
gradle.buildStarted(...) β
Kicking off, we have the gradle.buildStarted{}
hook. However, it doesn't spring into action for developers working outside the Gradle source code. Because the callback fires too early in the processβfollowing the creation of the Gradle
object but prior to the initialization of Settings
and each Project
object. It also beats the trigger of any external scripts, even init.gradle(.kts)
.
Due to these characteristics, this interface was flagged as deprecated in Gradle 6.x and was axed in version 7.0 to avoid muddying the waters for developers.
gradle.settingsEvaluated(...) β
This hook jumps into action after the execution of settings.gradle(.kts).
public class SettingsEvaluatedCallbackFiringSettingsProcessor ... {
...
@Override
public SettingsInternal process(GradleInternal gradle,
SettingsLocation settingsLocation,
ClassLoaderScope buildRootClassLoaderScope,
StartParameter startParameter) {
SettingsInternal settings = delegate.process(gradle,
settingsLocation, buildRootClassLoaderScope, startParameter);
gradle.getBuildListenerBroadcaster().settingsEvaluated(settings);
settings.preventFromFurtherMutation();
return settings;
}
}
Standard practice is to register this callback either within the Plugins imported via the init.gradle(.kts) script or directly within the settings.gradle(.kts). A closer inspection of the source code reveals that this hook's timing aligns almost perfectly with the conclusion of settings.gradle(.kts). Consequently, in typical projects devoid of any Init script or other Plugins in settings.gradle(.kts), functions intended for this hook can be placed at the tail end of settings.gradle(.kts). Here are some handy uses of this hook:
- Tweaking
ProjectDescription
: After usinginclude(String name)
in settings.gradle(.kts), Projects take their place in therootProject
of Settings, flaunting theirProjectDescription
. Here's a snapshot of theProjectDescription
API:
In Section 2-2, we showcased how to modify buildFileName
to declutter the build.gradle(.kts) display. You're also empowered to employ the ProjectDescription
API to switch compilation files across diverse environments or pinpoint specific project paths when ushering in external modules.
- Decreeing a universal
buildCache
setup: Consider a scenario where an organization allots each Gradle Project a remotebuildCache
space. Leveraging the standardized init.gradle(.kts) script, the pertinentbuildCache
setup can be appended to theSettings
object right after settings.gradle(.kts) concludes its execution. Check out morebuildCache
related commands in Section 4-5.
gradle.projectsLoaded(...) β
After the Settings initialization, once the ProjectDescription
is recognized, Gradle promptly loads the pertinent Module as defined by the ProjectDescription
. This then leads to the creation of the Project
object. The gradle.projectsLoaded{}
hook is tied to the Gradle object. Within its closure, we can access the loaded Project's root node via rootProject
, enabling traversal and access to all underlying Projects. Primarily, this callback highlights actions required before the Project's build script gets executed. Some typical uses are:
- Registering lifecycle listeners for the Project: This should ideally occur after the Project object has been instantiated but before its execution. While listeners for non-root Projects can be specified within the
rootProject
, specificrootProject
listeners (likebeforeProject()
,rootProject.tasks.whenTaskAdded()
, and so on) can only be defined within theprojectsLoaded
hook. - Introducing
buildscript
configuration to the Project. This includes the addition of dependencies, Plugins, and so on. As outlined in the preceding callback, using the init.gradle(.kts) script within the production CI environment allows for global Plugin integration. For instance, incorporating a performance analytics Plugin can be achieved timely through this callback, accessing eachProject
object and adding more custom logic.
4-1-3: Hooks in the Configuration Phase β
gradle.beforeProject(...) β
This hook largely mirrors its predecessor gradle.projectsLoaded(...)
, encompassing tasks like adding supplementary dependencies and Plugins. Here's what sets it apart:
- For the
gradle.projectsLoaded()
callback, traversing fromrootProject
is necessary to access the desiredProject
from the providedGradle
object. gradle.beforeProject()
is triggered prior to every Project's execution, passing in the respectiveProject
object.
project.beforeEvaluated(...) β
Though this hook and the one before it share the same trigger timing, they interact with distinct objects. While gradle.beforeProject()
offers a blanket setup for all Project
s, project.beforeEvaluated{}
mandates individual setups for each Project
. Key takeaways are:
- If there's a need to configure a specific Project only, this hook provides a more streamlined operation.
- In the sequence of operations,
gradle.beforeProject(...)
takes precedence, followed byproject.beforeEvaluated(...)
.
gradle.afterProject(...) β
This hook pairs with the gradle.beforeProject(...)
. For its extensive utilization, one can look directly into the project.afterEvaluated(...)
.
project.afterEvaluated(...) β
The project.afterEvaluated(...)
hook is commonly used in many Gradle scripts. On the one hand, since the DSL Extension and task graph aren't fully accessible during the configuration phase, this hook is typically used to read configurations in build.gradle(.kts). On the other hand, it also offers an advanced configuration opportunity for tasks that need the above extension data. A prime example is the Variant-related configuration in AGP, including the beforeVariants(...)
and onVariants(...)
discussed in Section 3-3, all of which occur within the AGP's preset afterEvaluated(...)
.
When an Android project introduces multiple Plugins, these could register numerous afterEvaluated(...)
callbacks. Given that these computations occur during the configuration phase, inclusive of the IDE sync process, it's imperative to use this hook judiciously to prevent potential performance issues.
Future chapters will showcase various uses of this hook. And in Section 8-1, we'll illustrate how to view all instances of afterEvaluate(...)
via the Gradle Scan Report.
gradle.projectsEvaluated(...) β
This hook, akin to gradle.projectsLoaded(...)
, activates right before task execution. Its usage closely mirrors that of the project.afterEvaluated(...)
, but its activation is post the completion of all Project evaluations rather than a singular project.afterEvaluate(...)
. This hook can also be seen as a global Project monitoring tool, allowing for data aggregation.
taskGraph.whenReady(...) β
Reflecting on our deep dive into the TaskExecutionGraph
in Section 2-2, this hook comes alive post the successful POPULATED
state. Consider it a final touchpoint for task configuration. Once all executable tasks are organized, Gradle grants an opportunity to modify them. For instance, you might want to disable certain tasks not required for the specific Variant.
4-1-4: Hooks in the Execution Phase β
taskGraph.beforeTask(...) β
This hook, living up to its name, provides a last-minute opportunity to tweak a task before its execution. One potential use-case is to inject APM (Application Performance Management) logging code in tandem with afterTask(...)
.
However, a word of caution is needed. Consider a hypothetical situation where the AGP has two intertwined tasks, A and B. If B relies on the output of A and you decide to intercept B's execution, changing its content just before its passage to B, it might seem to fulfill specific requirements. Yet, this approach is not compatible with Gradle's caching mechanism (the up-to-date check). A more refined strategy would involve placing a custom Task C between A and B, defining clear input and output parameters.
Do note that, post Gradle 7, this mechanism is also not compatible with Configuration Cache and is now deprecated.
taskActionListener.beforeAction(...) β
By its name, this hook is initiated just before any Task Action is executed. However, its practical application remains relatively rare. It's important to highlight that this mechanism doesn't support Configuration Cache and was deprecated in Gradle 7.
taskActionListener.afterAction(...) β
Similar to its "before" counterpart, this hook activates after any Task Action's execution. Its real-world application is sparse. Again, as with the previous hook, it's unsupported by Configuration Cache and deprecated as of Gradle 7.
taskGraph.afterTask(...) β
This hook springs into action subsequent to any Task's execution. Commonly, it's paired with beforeTask(...)
for the insertion of APM logging. Importantly, this hook remains active regardless of compilation results or error instances.
Furthermore, there exists an alternate set of listeners, as demonstrated below. These parallel the beforeTask(...)
and afterTask(...)
functionalities.
gradle.taskGraph.addTaskExecutionListener(
object : TaskExecutionListener {
override fun beforeExecute(task: Task) {}
override fun afterExecute(task: Task, state: TaskState) {}
})
However, post Gradle 7, this feature lacks Configuration Cache support and is on the deprecation list.
gradle.buildFinished(...) β
This hook is the curtain call, activated once all Gradle build logic concludes. It's predominantly utilized for concluding tasks such as:
- Purging superfluous intermediate files on the CI/CD server.
- Aggregating a comprehensive report of the compilation process and forwarding it to a remote server.
This hook is steadfast in its operation, unaffected by compilation outcomes or potential errors. However, as with many hooks, post Gradle 7, their compatibility with Configuration Cache is terminated. Check the "Event Notification" portion in this section for its alternative.
4-1-5: In Summary β
The intricacies of Gradle's lifecycle hooks warrant careful attention. It's essential to grasp their potential impact, especially concerning the Configuration phase. As we transition beyond Gradle 7, the array of deployable hooks has dwindled. The newer Event Notification model remains in anticipation of further API extensions.