All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.gradle.api.internaltransform.DefaultTransformer Maven / Gradle / Ivy

There is a newer version: 8.6
Show newest version
/*
 * Copyright 2019 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.artifacts.transform;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.reflect.TypeToken;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.artifacts.transform.InputArtifact;
import org.gradle.api.artifacts.transform.InputArtifactDependencies;
import org.gradle.api.artifacts.transform.TransformAction;
import org.gradle.api.artifacts.transform.TransformParameters;
import org.gradle.api.artifacts.transform.VariantTransformConfigurationException;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.internal.DomainObjectContext;
import org.gradle.api.internal.attributes.ImmutableAttributes;
import org.gradle.api.internal.file.FileCollectionFactory;
import org.gradle.api.internal.file.FileLookup;
import org.gradle.api.internal.plugins.DslObject;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.api.internal.tasks.NodeExecutionContext;
import org.gradle.api.internal.tasks.TaskDependencyResolveContext;
import org.gradle.api.internal.tasks.properties.FileParameterUtils;
import org.gradle.api.internal.tasks.properties.InputFilePropertyType;
import org.gradle.api.internal.tasks.properties.InputParameterUtils;
import org.gradle.api.internal.tasks.properties.OutputFilePropertyType;
import org.gradle.api.internal.tasks.properties.PropertyValue;
import org.gradle.api.internal.tasks.properties.PropertyVisitor;
import org.gradle.api.internal.tasks.properties.PropertyWalker;
import org.gradle.api.provider.Provider;
import org.gradle.api.reflect.InjectionPointQualifier;
import org.gradle.api.tasks.FileNormalizer;
import org.gradle.internal.Describables;
import org.gradle.internal.deprecation.DeprecationLogger;
import org.gradle.internal.exceptions.DefaultMultiCauseException;
import org.gradle.internal.fingerprint.AbsolutePathInputNormalizer;
import org.gradle.internal.fingerprint.CurrentFileCollectionFingerprint;
import org.gradle.internal.fingerprint.FileCollectionFingerprinter;
import org.gradle.internal.fingerprint.FileCollectionFingerprinterRegistry;
import org.gradle.internal.fingerprint.FileNormalizationSpec;
import org.gradle.internal.fingerprint.impl.DefaultFileNormalizationSpec;
import org.gradle.internal.fingerprint.DirectorySensitivity;
import org.gradle.internal.hash.ClassLoaderHierarchyHasher;
import org.gradle.internal.hash.HashCode;
import org.gradle.internal.hash.Hasher;
import org.gradle.internal.hash.Hashing;
import org.gradle.internal.instantiation.InstanceFactory;
import org.gradle.internal.instantiation.InstantiationScheme;
import org.gradle.internal.isolated.IsolationScheme;
import org.gradle.internal.isolation.Isolatable;
import org.gradle.internal.isolation.IsolatableFactory;
import org.gradle.internal.logging.text.TreeFormatter;
import org.gradle.internal.model.CalculatedValueContainer;
import org.gradle.internal.model.CalculatedValueContainerFactory;
import org.gradle.internal.model.ModelContainer;
import org.gradle.internal.model.ValueCalculator;
import org.gradle.internal.operations.BuildOperationContext;
import org.gradle.internal.operations.BuildOperationDescriptor;
import org.gradle.internal.operations.BuildOperationExecutor;
import org.gradle.internal.operations.BuildOperationType;
import org.gradle.internal.operations.RunnableBuildOperation;
import org.gradle.internal.reflect.DefaultTypeValidationContext;
import org.gradle.internal.reflect.TypeValidationContext;
import org.gradle.internal.service.ServiceLookup;
import org.gradle.internal.service.ServiceLookupException;
import org.gradle.internal.service.UnknownServiceException;
import org.gradle.internal.snapshot.ValueSnapshot;
import org.gradle.internal.snapshot.ValueSnapshotter;
import org.gradle.model.internal.type.ModelType;
import org.gradle.work.InputChanges;

import javax.annotation.Nullable;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static org.gradle.internal.reflect.TypeValidationContext.Severity.WARNING;

public class DefaultTransformer extends AbstractTransformer> {

    private final Class fileNormalizer;
    private final Class dependenciesNormalizer;
    private final FileLookup fileLookup;
    private final ServiceLookup internalServices;
    private final boolean requiresDependencies;
    private final boolean requiresInputChanges;
    private final InstanceFactory> instanceFactory;
    private final boolean cacheable;
    private final CalculatedValueContainer isolatedParameters;
    private final DirectorySensitivity artifactDirectorySensitivity;
    private final DirectorySensitivity dependenciesDirectorySensitivity;

    public DefaultTransformer(
        Class> implementationClass,
        @Nullable TransformParameters parameterObject,
        ImmutableAttributes fromAttributes,
        Class inputArtifactNormalizer,
        Class dependenciesNormalizer,
        boolean cacheable,
        DirectorySensitivity artifactDirectorySensitivity,
        DirectorySensitivity dependenciesDirectorySensitivity,
        BuildOperationExecutor buildOperationExecutor,
        ClassLoaderHierarchyHasher classLoaderHierarchyHasher,
        IsolatableFactory isolatableFactory,
        ValueSnapshotter valueSnapshotter,
        FileCollectionFactory fileCollectionFactory,
        FileLookup fileLookup,
        PropertyWalker parameterPropertyWalker,
        InstantiationScheme actionInstantiationScheme,
        DomainObjectContext owner,
        CalculatedValueContainerFactory calculatedValueContainerFactory,
        ServiceLookup internalServices
    ) {
        super(implementationClass, fromAttributes);
        this.fileNormalizer = inputArtifactNormalizer;
        this.dependenciesNormalizer = dependenciesNormalizer;
        this.fileLookup = fileLookup;
        this.internalServices = internalServices;
        this.instanceFactory = actionInstantiationScheme.forType(implementationClass);
        this.requiresDependencies = instanceFactory.serviceInjectionTriggeredByAnnotation(InputArtifactDependencies.class);
        this.requiresInputChanges = instanceFactory.requiresService(InputChanges.class);
        this.cacheable = cacheable;
        this.artifactDirectorySensitivity = artifactDirectorySensitivity;
        this.dependenciesDirectorySensitivity = dependenciesDirectorySensitivity;
        this.isolatedParameters = calculatedValueContainerFactory.create(Describables.of("parameters of", this),
            new IsolateTransformerParameters(parameterObject, implementationClass, cacheable, owner, parameterPropertyWalker, isolatableFactory, buildOperationExecutor, classLoaderHierarchyHasher,
                valueSnapshotter, fileCollectionFactory));
    }

    /**
     * Used to recreate a transformer from the configuration cache.
     */
    public DefaultTransformer(
        Class> implementationClass,
        CalculatedValueContainer isolatedParameters,
        ImmutableAttributes fromAttributes,
        Class inputArtifactNormalizer,
        Class dependenciesNormalizer,
        boolean cacheable,
        FileLookup fileLookup,
        InstantiationScheme actionInstantiationScheme,
        ServiceLookup internalServices,
        DirectorySensitivity artifactDirectorySensitivity,
        DirectorySensitivity dependenciesDirectorySensitivity) {
        super(implementationClass, fromAttributes);
        this.fileNormalizer = inputArtifactNormalizer;
        this.dependenciesNormalizer = dependenciesNormalizer;
        this.fileLookup = fileLookup;
        this.internalServices = internalServices;
        this.instanceFactory = actionInstantiationScheme.forType(implementationClass);
        this.requiresDependencies = instanceFactory.serviceInjectionTriggeredByAnnotation(InputArtifactDependencies.class);
        this.requiresInputChanges = instanceFactory.requiresService(InputChanges.class);
        this.cacheable = cacheable;
        this.isolatedParameters = isolatedParameters;
        this.artifactDirectorySensitivity = artifactDirectorySensitivity;
        this.dependenciesDirectorySensitivity = dependenciesDirectorySensitivity;
    }

    public static void validateInputFileNormalizer(String propertyName, @Nullable Class normalizer, boolean cacheable, TypeValidationContext validationContext) {
        if (cacheable) {
            if (normalizer == AbsolutePathInputNormalizer.class) {
                validationContext.visitPropertyProblem(WARNING,
                    propertyName,
                    "is declared to be sensitive to absolute paths. This is not allowed for cacheable transforms"
                );
            }
        }
    }

    @Override
    public Class getInputArtifactNormalizer() {
        return fileNormalizer;
    }

    @Override
    public Class getInputArtifactDependenciesNormalizer() {
        return dependenciesNormalizer;
    }

    @Override
    public boolean isIsolated() {
        return isolatedParameters.getOrNull() != null;
    }

    @Override
    public boolean requiresDependencies() {
        return requiresDependencies;
    }

    @Override
    public boolean requiresInputChanges() {
        return requiresInputChanges;
    }

    @Override
    public boolean isCacheable() {
        return cacheable;
    }

    @Override
    public DirectorySensitivity getInputArtifactDirectorySensitivity() {
        return artifactDirectorySensitivity;
    }

    @Override
    public DirectorySensitivity getInputArtifactDependenciesDirectorySensitivity() {
        return dependenciesDirectorySensitivity;
    }

    @Override
    public HashCode getSecondaryInputHash() {
        return isolatedParameters.get().getSecondaryInputsHash();
    }

    @Override
    public ImmutableList transform(Provider inputArtifactProvider, File outputDir, ArtifactTransformDependencies dependencies, @Nullable InputChanges inputChanges) {
        TransformAction transformAction = newTransformAction(inputArtifactProvider, dependencies, inputChanges);
        DefaultTransformOutputs transformOutputs = new DefaultTransformOutputs(inputArtifactProvider.get().getAsFile(), outputDir, fileLookup);
        transformAction.transform(transformOutputs);
        return transformOutputs.getRegisteredOutputs();
    }

    @Override
    public void visitDependencies(TaskDependencyResolveContext context) {
        context.add(isolatedParameters);
    }

    @Override
    public void isolateParametersIfNotAlready() {
        isolatedParameters.finalizeIfNotAlready();
    }

    private static void fingerprintParameters(
        ValueSnapshotter valueSnapshotter,
        FileCollectionFingerprinterRegistry fingerprinterRegistry,
        FileCollectionFactory fileCollectionFactory,
        PropertyWalker propertyWalker,
        Hasher hasher,
        Object parameterObject,
        boolean cacheable
    ) {
        ImmutableSortedMap.Builder inputParameterFingerprintsBuilder = ImmutableSortedMap.naturalOrder();
        ImmutableSortedMap.Builder inputFileParameterFingerprintsBuilder = ImmutableSortedMap.naturalOrder();
        DefaultTypeValidationContext validationContext = DefaultTypeValidationContext.withoutRootType(cacheable);
        propertyWalker.visitProperties(parameterObject, validationContext, new PropertyVisitor.Adapter() {
            @Override
            public void visitInputProperty(String propertyName, PropertyValue value, boolean optional) {
                try {
                    Object preparedValue = InputParameterUtils.prepareInputParameterValue(value);

                    if (preparedValue == null && !optional) {
                        validationContext.visitPropertyProblem(WARNING,
                            propertyName,
                            "does not have a value specified"
                        );
                    }

                    inputParameterFingerprintsBuilder.put(propertyName, valueSnapshotter.snapshot(preparedValue));
                } catch (Throwable e) {
                    throw new InvalidUserDataException(String.format(
                        "Error while evaluating property '%s' of %s",
                        propertyName,
                        getParameterObjectDisplayName(parameterObject)
                    ), e);
                }
            }

            @Override
            public void visitOutputFileProperty(String propertyName, boolean optional, PropertyValue value, OutputFilePropertyType filePropertyType) {
                validationContext.visitPropertyProblem(WARNING,
                    propertyName,
                    "is annotated with an output annotation"
                );
            }

            @Override
            public void visitInputFileProperty(String propertyName, boolean optional, boolean skipWhenEmpty, DirectorySensitivity directorySensitivity, boolean incremental, @Nullable Class fileNormalizer, PropertyValue value, InputFilePropertyType filePropertyType) {
                validateInputFileNormalizer(propertyName, fileNormalizer, cacheable, validationContext);
                FileNormalizationSpec fileNormalizationSpec = DefaultFileNormalizationSpec.from(FileParameterUtils.normalizerOrDefault(fileNormalizer), directorySensitivity);
                FileCollectionFingerprinter fingerprinter = fingerprinterRegistry.getFingerprinter(fileNormalizationSpec);
                FileCollection inputFileValue = FileParameterUtils.resolveInputFileValue(fileCollectionFactory, filePropertyType, value);
                CurrentFileCollectionFingerprint fingerprint = fingerprinter.fingerprint(inputFileValue);
                inputFileParameterFingerprintsBuilder.put(propertyName, fingerprint);
            }
        });

        ImmutableMap validationMessages = validationContext.getProblems();
        if (!validationMessages.isEmpty()) {
            throw new DefaultMultiCauseException(
                String.format(validationMessages.size() == 1 ? "A problem was found with the configuration of the artifact transform parameter %s." : "Some problems were found with the configuration of the artifact transform parameter %s.", getParameterObjectDisplayName(parameterObject)),
                validationMessages.keySet().stream().sorted().map(InvalidUserDataException::new).collect(Collectors.toList())
            );
        }

        for (Map.Entry entry : inputParameterFingerprintsBuilder.build().entrySet()) {
            hasher.putString(entry.getKey());
            entry.getValue().appendToHasher(hasher);
        }
        for (Map.Entry entry : inputFileParameterFingerprintsBuilder.build().entrySet()) {
            hasher.putString(entry.getKey());
            hasher.putHash(entry.getValue().getHash());
        }
    }

    private static String getParameterObjectDisplayName(Object parameterObject) {
        return ModelType.of(new DslObject(parameterObject).getDeclaredType()).getDisplayName();
    }

    private TransformAction newTransformAction(Provider inputArtifactProvider, ArtifactTransformDependencies artifactTransformDependencies, @Nullable InputChanges inputChanges) {
        TransformParameters parameters = isolatedParameters.get().getIsolatedParameterObject().isolate();
        ServiceLookup services = new IsolationScheme<>(TransformAction.class, TransformParameters.class, TransformParameters.None.class).servicesForImplementation(parameters, internalServices);
        services = new TransformServiceLookup(inputArtifactProvider, requiresDependencies ? artifactTransformDependencies : null, inputChanges, services);
        return instanceFactory.newInstance(services);
    }

    public CalculatedValueContainer getIsolatedParameters() {
        return isolatedParameters;
    }

    private static class TransformServiceLookup implements ServiceLookup {
        private static final Type FILE_SYSTEM_LOCATION_PROVIDER = new TypeToken>() {
        }.getType();

        private final ImmutableList injectionPoints;
        private final ServiceLookup delegate;

        public TransformServiceLookup(Provider inputFileProvider, @Nullable ArtifactTransformDependencies artifactTransformDependencies, @Nullable InputChanges inputChanges, ServiceLookup delegate) {
            this.delegate = delegate;
            ImmutableList.Builder builder = ImmutableList.builder();
            builder.add(InjectionPoint.injectedByAnnotation(InputArtifact.class, File.class, () -> {
                DeprecationLogger
                    .deprecate("Injecting the input artifact of a transform as a File")
                    .withAdvice("Declare the input artifact as Provider instead.")
                    .willBeRemovedInGradle7()
                    .withUserManual("artifact_transforms", "sec:implementing-artifact-transforms")
                    .nagUser();
                return inputFileProvider.get().getAsFile();
            }));
            builder.add(InjectionPoint.injectedByAnnotation(InputArtifact.class, FILE_SYSTEM_LOCATION_PROVIDER, () -> inputFileProvider));
            if (artifactTransformDependencies != null) {
                builder.add(InjectionPoint.injectedByAnnotation(InputArtifactDependencies.class, artifactTransformDependencies::getFiles));
            }
            if (inputChanges != null) {
                builder.add(InjectionPoint.injectedByType(InputChanges.class, () -> inputChanges));
            }
            this.injectionPoints = builder.build();
        }

        @Nullable
        private Object find(Type serviceType, @Nullable Class annotatedWith) {
            TypeToken serviceTypeToken = TypeToken.of(serviceType);
            for (InjectionPoint injectionPoint : injectionPoints) {
                if (annotatedWith == injectionPoint.getAnnotation() && serviceTypeToken.isSupertypeOf(injectionPoint.getInjectedType())) {
                    return injectionPoint.getValueToInject();
                }
            }
            return null;
        }

        @Nullable
        @Override
        public Object find(Type serviceType) throws ServiceLookupException {
            Object result = find(serviceType, null);
            if (result != null) {
                return result;
            }
            return delegate.find(serviceType);
        }

        @Override
        public Object get(Type serviceType) throws UnknownServiceException, ServiceLookupException {
            Object result = find(serviceType);
            if (result == null) {
                throw new UnknownServiceException(serviceType, "No service of type " + serviceType + " available.");
            }
            return result;
        }

        @Override
        public Object get(Type serviceType, Class annotatedWith) throws UnknownServiceException, ServiceLookupException {
            Object result = find(serviceType, annotatedWith);
            if (result != null) {
                return result;
            }
            return delegate.get(serviceType, annotatedWith);
        }

        private static class InjectionPoint {
            private final Class annotation;
            private final Type injectedType;
            private final Supplier valueToInject;

            public static InjectionPoint injectedByAnnotation(Class annotation, Supplier valueToInject) {
                return new InjectionPoint(annotation, determineTypeFromAnnotation(annotation), valueToInject);
            }

            public static InjectionPoint injectedByAnnotation(Class annotation, Type injectedType, Supplier valueToInject) {
                return new InjectionPoint(annotation, injectedType, valueToInject);
            }

            public static InjectionPoint injectedByType(Class injectedType, Supplier valueToInject) {
                return new InjectionPoint(null, injectedType, valueToInject);
            }

            private InjectionPoint(@Nullable Class annotation, Type injectedType, Supplier valueToInject) {
                this.annotation = annotation;
                this.injectedType = injectedType;
                this.valueToInject = valueToInject;
            }

            private static Class determineTypeFromAnnotation(Class annotation) {
                Class[] supportedTypes = annotation.getAnnotation(InjectionPointQualifier.class).supportedTypes();
                if (supportedTypes.length != 1) {
                    throw new IllegalArgumentException("Cannot determine supported type for annotation " + annotation.getName());
                }
                return supportedTypes[0];
            }

            @Nullable
            public Class getAnnotation() {
                return annotation;
            }

            public Type getInjectedType() {
                return injectedType;
            }

            public Object getValueToInject() {
                return valueToInject.get();
            }
        }
    }

    public static class IsolatedParameters {
        private final HashCode secondaryInputsHash;
        private final Isolatable isolatedParameterObject;

        public IsolatedParameters(Isolatable isolatedParameterObject, HashCode secondaryInputsHash) {
            this.secondaryInputsHash = secondaryInputsHash;
            this.isolatedParameterObject = isolatedParameterObject;
        }

        public HashCode getSecondaryInputsHash() {
            return secondaryInputsHash;
        }

        public Isolatable getIsolatedParameterObject() {
            return isolatedParameterObject;
        }
    }

    public static class IsolateTransformerParameters implements ValueCalculator {
        private final TransformParameters parameterObject;
        private final DomainObjectContext owner;
        private final IsolatableFactory isolatableFactory;
        private final PropertyWalker parameterPropertyWalker;
        private final BuildOperationExecutor buildOperationExecutor;
        private final ClassLoaderHierarchyHasher classLoaderHierarchyHasher;
        private final ValueSnapshotter valueSnapshotter;
        private final FileCollectionFactory fileCollectionFactory;
        private final boolean cacheable;
        private final Class implementationClass;

        public IsolateTransformerParameters(@Nullable TransformParameters parameterObject,
                                            Class implementationClass,
                                            boolean cacheable,
                                            DomainObjectContext owner,
                                            PropertyWalker parameterPropertyWalker,
                                            IsolatableFactory isolatableFactory,
                                            BuildOperationExecutor buildOperationExecutor,
                                            ClassLoaderHierarchyHasher classLoaderHierarchyHasher,
                                            ValueSnapshotter valueSnapshotter,
                                            FileCollectionFactory fileCollectionFactory) {
            this.parameterObject = parameterObject;
            this.implementationClass = implementationClass;
            this.cacheable = cacheable;
            this.owner = owner;
            this.parameterPropertyWalker = parameterPropertyWalker;
            this.isolatableFactory = isolatableFactory;
            this.buildOperationExecutor = buildOperationExecutor;
            this.classLoaderHierarchyHasher = classLoaderHierarchyHasher;
            this.valueSnapshotter = valueSnapshotter;
            this.fileCollectionFactory = fileCollectionFactory;
        }

        @Nullable
        public TransformParameters getParameterObject() {
            return parameterObject;
        }

        public boolean isCacheable() {
            return cacheable;
        }

        public Class getImplementationClass() {
            return implementationClass;
        }

        @Override
        public boolean usesMutableProjectState() {
            return owner.getProject() != null;
        }

        @Nullable
        @Override
        public ProjectInternal getOwningProject() {
            return owner.getProject();
        }

        @Override
        public void visitDependencies(TaskDependencyResolveContext context) {
            if (parameterObject != null) {
                parameterPropertyWalker.visitProperties(parameterObject, TypeValidationContext.NOOP, new PropertyVisitor.Adapter() {
                    @Override
                    public void visitInputFileProperty(
                        String propertyName,
                        boolean optional,
                        boolean skipWhenEmpty,
                        DirectorySensitivity directorySensitivity,
                        boolean incremental,
                        @Nullable Class fileNormalizer,
                        PropertyValue value,
                        InputFilePropertyType filePropertyType
                    ) {
                        context.add(value.getTaskDependencies());
                    }
                });
            }
        }

        @Override
        public IsolatedParameters calculateValue(NodeExecutionContext context) {
            FileCollectionFingerprinterRegistry fingerprinterRegistry = context.getService(FileCollectionFingerprinterRegistry.class);
            return isolateParameters(fingerprinterRegistry);
        }

        private IsolatedParameters isolateParameters(FileCollectionFingerprinterRegistry fingerprinterRegistry) {
            ModelContainer model = owner.getModel();
            if (!model.hasMutableState()) {
                // This may happen when a task visits artifacts using a FileCollection instance created from a Configuration instance in a different project (not an artifact produced by a different project, these work fine)
                // There is a check in DefaultConfiguration that deprecates resolving dependencies via FileCollection instance created by a different project, however that check may not
                // necessarily be triggered. For example, the configuration may be legitimately resolved by some other task prior to the problematic task running
                // TODO - hoist this up into configuration file collection visiting (and not when visiting the upstream dependencies of a transform), and deprecate this in Gradle 7.x
                //
                // This may also happen when a transform takes upstream dependencies and the dependencies are transformed using a different transform
                // In this case, the main thread that schedules the work should isolate the transform parameters prior to scheduling the work. However, the dependencies may
                // be filtered from the result, so that the transform is not visited by the main thread, or the transform worker may start work before the main thread
                // has a chance to isolate the upstream transform
                // TODO - ensure all transform parameters required by a transform worker are isolated prior to starting the worker
                //
                // Force access to the state of the owner, regardless of whether any other thread has access. This is because attempting to acquire a lock for a project may deadlock
                // when performed from a worker thread (see DefaultBuildOperationQueue.waitForCompletion() which intentionally does not release the project locks while waiting)
                // TODO - add validation to fail eagerly when a worker attempts to lock a project
                //
                return model.forceAccessToMutableState(o -> doIsolateParameters(fingerprinterRegistry));
            } else {
                return doIsolateParameters(fingerprinterRegistry);
            }
        }

        private IsolatedParameters doIsolateParameters(FileCollectionFingerprinterRegistry fingerprinterRegistry) {
            try {
                return isolateParametersExclusively(fingerprinterRegistry);
            } catch (Exception e) {
                TreeFormatter formatter = new TreeFormatter();
                formatter.node("Could not isolate parameters ").appendValue(parameterObject).append(" of artifact transform ").appendType(implementationClass);
                throw new VariantTransformConfigurationException(formatter.toString(), e);
            }
        }

        private IsolatedParameters isolateParametersExclusively(FileCollectionFingerprinterRegistry fingerprinterRegistry) {
            Isolatable isolatedParameterObject = isolatableFactory.isolate(parameterObject);

            Hasher hasher = Hashing.newHasher();
            appendActionImplementation(implementationClass, hasher, classLoaderHierarchyHasher);

            if (parameterObject != null) {
                TransformParameters isolatedTransformParameters = isolatedParameterObject.isolate();
                buildOperationExecutor.run(new RunnableBuildOperation() {
                    @Override
                    public void run(BuildOperationContext context) {
                        // TODO wolfs - schedule fingerprinting separately, it can be done without having the project lock
                        fingerprintParameters(
                            valueSnapshotter,
                            fingerprinterRegistry,
                            fileCollectionFactory,
                            parameterPropertyWalker,
                            hasher,
                            isolatedTransformParameters,
                            cacheable
                        );
                        context.setResult(FingerprintTransformInputsOperation.Result.INSTANCE);
                    }

                    @Override
                    public BuildOperationDescriptor.Builder description() {
                        return BuildOperationDescriptor
                            .displayName("Fingerprint transformation inputs")
                            .details(FingerprintTransformInputsOperation.Details.INSTANCE);
                    }
                });
            }
            HashCode secondaryInputsHash = hasher.hash();
            return new IsolatedParameters(isolatedParameterObject, secondaryInputsHash);
        }
    }

    /*
     * This operation is only used here temporarily. Should be replaced with a more stable operation in the long term.
     */
    public interface FingerprintTransformInputsOperation extends BuildOperationType {
        interface Details {
            Details INSTANCE = new Details() {
            };
        }

        interface Result {
            Result INSTANCE = new Result() {
            };
        }
    }
}