/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.api.internal.tasks.execution;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.hash.HashCode;
import org.gradle.api.Nullable;
import org.gradle.api.internal.TaskInternal;
import org.gradle.api.internal.changedetection.TaskArtifactState;
import org.gradle.api.internal.tasks.SnapshotTaskInputsBuildOperationType;
import org.gradle.api.internal.tasks.TaskExecuter;
import org.gradle.api.internal.tasks.TaskExecutionContext;
import org.gradle.api.internal.tasks.TaskStateInternal;
import org.gradle.caching.internal.tasks.TaskOutputCachingBuildCacheKey;
import org.gradle.internal.operations.BuildOperationContext;
import org.gradle.internal.operations.BuildOperationExecutor;
import org.gradle.internal.operations.RunnableBuildOperation;
import org.gradle.internal.progress.BuildOperationDescriptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
public class ResolveBuildCacheKeyExecuter implements TaskExecuter {
private static final Logger LOGGER = LoggerFactory.getLogger(ResolveBuildCacheKeyExecuter.class);
private final TaskExecuter delegate;
private final BuildOperationExecutor buildOperationExecutor;
public ResolveBuildCacheKeyExecuter(TaskExecuter delegate, BuildOperationExecutor buildOperationExecutor) {
this.delegate = delegate;
this.buildOperationExecutor = buildOperationExecutor;
}
@Override
public void execute(TaskInternal task, TaskStateInternal state, TaskExecutionContext context) {
resolve(task, context);
delegate.execute(task, state, context);
}
private void resolve(final TaskInternal task, final TaskExecutionContext context) {
/*
This operation represents the work of analyzing the inputs.
Therefore, it should encompass all of the file IO and compute necessary to do this.
This effectively happens in the first call to context.getTaskArtifactState().getStates().
If build caching is enabled, this is the first time that this will be called so it effectively
encapsulates this work.
If build cache isn't enabled, this executer isn't in the mix and therefore the work of hashing
the inputs will happen later in the executer chain, and therefore they aren't wrapped in an operation.
We avoid adding this executer if build caching is not enabled due to concerns of performance impact
So, later, we either need always have this executer in the mix or make the input hashing
an explicit step that always happens earlier and wrap it.
Regardless, it would be good to formalise the input work in some form so it doesn't just
happen as a side effect of calling some method for the first time.
*/
buildOperationExecutor.run(new RunnableBuildOperation() {
@Override
public void run(BuildOperationContext buildOperationContext) {
TaskOutputCachingBuildCacheKey cacheKey = doResolve(task, context);
buildOperationContext.setResult(new OperationResultImpl(cacheKey));
context.setBuildCacheKey(cacheKey);
}
@Override
public BuildOperationDescriptor.Builder description() {
return BuildOperationDescriptor
.displayName("Snapshot task inputs for " + task.getIdentityPath())
.details(new OperationDetailsImpl(task));
}
});
}
private TaskOutputCachingBuildCacheKey doResolve(TaskInternal task, TaskExecutionContext context) {
TaskArtifactState taskState = context.getTaskArtifactState();
TaskOutputCachingBuildCacheKey cacheKey = taskState.calculateCacheKey();
if (task.getOutputs().getHasOutput()) { // A task with no outputs an no cache key.
if (cacheKey.isValid()) {
LOGGER.info("Build cache key for {} is {}", task, cacheKey.getHashCode());
}
}
return cacheKey;
}
private static class OperationDetailsImpl implements SnapshotTaskInputsBuildOperationType.Details {
private final TaskInternal task;
private OperationDetailsImpl(TaskInternal task) {
this.task = task;
}
@Override
public String getTaskPath() {
return task.getIdentityPath().getPath();
}
@Override
public long getTaskId() {
return System.identityHashCode(task);
}
}
@VisibleForTesting
static class OperationResultImpl implements SnapshotTaskInputsBuildOperationType.Result {
@VisibleForTesting
final TaskOutputCachingBuildCacheKey key;
OperationResultImpl(TaskOutputCachingBuildCacheKey key) {
this.key = key;
}
@Nullable
@Override
public Map<String, String> getInputHashes() {
ImmutableSortedMap<String, HashCode> inputHashes = key.getInputs().getInputHashes();
if (inputHashes == null || inputHashes.isEmpty()) {
return null;
} else {
return Maps.transformValues(inputHashes, new Function<HashCode, String>() {
@Override
public String apply(HashCode input) {
return input.toString();
}
});
}
}
@Nullable
@Override
public String getClassLoaderHash() {
HashCode classLoaderHash = key.getInputs().getClassLoaderHash();
return classLoaderHash == null ? null : classLoaderHash.toString();
}
@Nullable
@Override
public List<String> getActionClassLoaderHashes() {
List<HashCode> actionClassLoaderHashes = key.getInputs().getActionClassLoaderHashes();
if (actionClassLoaderHashes == null || actionClassLoaderHashes.isEmpty()) {
return null;
} else {
return Lists.transform(actionClassLoaderHashes, new Function<HashCode, String>() {
@Override
public String apply(HashCode input) {
return input == null ? null : input.toString();
}
});
}
}
@Nullable
@Override
public List<String> getActionClassNames() {
ImmutableList<String> actionClassNames = key.getInputs().getActionClassNames();
if (actionClassNames == null || actionClassNames.isEmpty()) {
return null;
} else {
return actionClassNames;
}
}
@Nullable
@Override
public List<String> getOutputPropertyNames() {
// Copy should be a NOOP as this is an immutable sorted set upstream.
ImmutableSortedSet<String> outputPropertyNames = key.getInputs().getOutputPropertyNames();
if (outputPropertyNames == null || outputPropertyNames.isEmpty()) {
return null;
} else {
return ImmutableSortedSet.copyOf(outputPropertyNames).asList();
}
}
@Nullable
@Override
public String getBuildCacheKey() {
return key.isValid() ? key.getHashCode() : null;
}
}
}