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

org.gradle.groovy.scripts.internal.GroovyScriptClassCompiler Maven / Gradle / Ivy

There is a newer version: 8.11.1
Show newest version
/*
 * Copyright 2011 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.groovy.scripts.internal;

import groovy.lang.Script;
import org.codehaus.groovy.ast.ClassNode;
import org.gradle.api.Action;
import org.gradle.api.file.RelativePath;
import org.gradle.api.internal.file.FileCollectionFactory;
import org.gradle.api.internal.initialization.ClassLoaderScope;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.groovy.scripts.ScriptSource;
import org.gradle.groovy.scripts.internal.GroovyScriptClassCompiler.GroovyScriptCompilationAndInstrumentation.GroovyScriptCompilationOutput;
import org.gradle.internal.Pair;
import org.gradle.internal.classanalysis.AsmConstants;
import org.gradle.internal.classpath.CachedClasspathTransformer;
import org.gradle.internal.classpath.ClassData;
import org.gradle.internal.classpath.ClassPath;
import org.gradle.internal.classpath.ClasspathEntryVisitor;
import org.gradle.internal.classpath.DefaultClassPath;
import org.gradle.internal.classpath.transforms.ClassTransform;
import org.gradle.internal.classpath.transforms.ClasspathElementTransformFactoryForLegacy;
import org.gradle.internal.execution.ExecutionEngine;
import org.gradle.internal.execution.InputFingerprinter;
import org.gradle.internal.execution.UnitOfWork;
import org.gradle.internal.execution.caching.CachingDisabledReason;
import org.gradle.internal.execution.history.OverlappingOutputs;
import org.gradle.internal.execution.workspace.ImmutableWorkspaceProvider;
import org.gradle.internal.file.TreeType;
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.scripts.BuildScriptCompilationAndInstrumentation;
import org.gradle.model.dsl.internal.transform.RuleVisitor;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import javax.annotation.Nullable;
import java.io.Closeable;
import java.io.File;
import java.net.URI;
import java.util.Optional;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * A {@link ScriptClassCompiler} which compiles scripts to a cache directory, and loads them from there.
 */
public class GroovyScriptClassCompiler implements ScriptClassCompiler, Closeable {
    private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
    private static final String CLASSPATH_PROPERTY_NAME = "classpath";
    private static final String TEMPLATE_ID_PROPERTY_NAME = "templateId";
    private static final String SOURCE_HASH_PROPERTY_NAME = "sourceHash";
    private final ScriptCompilationHandler scriptCompilationHandler;
    private final ClassLoaderHierarchyHasher classLoaderHierarchyHasher;
    private final CachedClasspathTransformer classpathTransformer;
    private final ExecutionEngine earlyExecutionEngine;
    private final FileCollectionFactory fileCollectionFactory;
    private final InputFingerprinter inputFingerprinter;
    private final ImmutableWorkspaceProvider workspaceProvider;
    private final ClasspathElementTransformFactoryForLegacy transformFactoryForLegacy;

    public GroovyScriptClassCompiler(
        ScriptCompilationHandler scriptCompilationHandler,
        ClassLoaderHierarchyHasher classLoaderHierarchyHasher,
        CachedClasspathTransformer classpathTransformer,
        ExecutionEngine earlyExecutionEngine,
        FileCollectionFactory fileCollectionFactory,
        InputFingerprinter inputFingerprinter,
        ImmutableWorkspaceProvider workspaceProvider,
        ClasspathElementTransformFactoryForLegacy transformFactoryForLegacy
    ) {
        this.scriptCompilationHandler = scriptCompilationHandler;
        this.classLoaderHierarchyHasher = classLoaderHierarchyHasher;
        this.classpathTransformer = classpathTransformer;
        this.earlyExecutionEngine = earlyExecutionEngine;
        this.fileCollectionFactory = fileCollectionFactory;
        this.inputFingerprinter = inputFingerprinter;
        this.workspaceProvider = workspaceProvider;
        this.transformFactoryForLegacy = transformFactoryForLegacy;
    }

    @Override
    public  CompiledScript compile(
        final ScriptSource source, final Class scriptBaseClass, final Object target,
        final ClassLoaderScope targetScope,
        final CompileOperation operation,
        final Action verifier
    ) {
        assert source.getResource().isContentCached();
        if (source.getResource().getHasEmptyContent()) {
            return new EmptyCompiledScript<>(operation);
        }

        String templateId = operation.getId();
        // TODO: Figure if execution engine should calculate the source hash on its own
        HashCode sourceHashCode = source.getResource().getContentHash();
        RemappingScriptSource remapped = new RemappingScriptSource(source);
        ClassLoader classLoader = targetScope.getExportClassLoader();
        GroovyScriptCompilationOutput output = doCompile(target, templateId, sourceHashCode, remapped, classLoader, operation, verifier, scriptBaseClass);

        File instrumentedOutput = output.getInstrumentedOutput();
        File metadataDir = output.getMetadataDir();
        // TODO: Remove the remapping or move remapping to an uncached unit of work?
        ClassPath remappedClasses = remapClasses(instrumentedOutput, remapped);
        return scriptCompilationHandler.loadFromDir(source, sourceHashCode, targetScope, remappedClasses, metadataDir, operation, scriptBaseClass);
    }

    private  GroovyScriptCompilationOutput doCompile(
        Object target,
        String templateId,
        HashCode sourceHashCode,
        RemappingScriptSource source,
        ClassLoader classLoader,
        CompileOperation operation,
        Action verifier,
        Class scriptBaseClass
    ) {
        UnitOfWork unitOfWork = new GroovyScriptCompilationAndInstrumentation(
            templateId,
            sourceHashCode,
            classLoader,
            source,
            operation,
            verifier,
            scriptBaseClass,
            classLoaderHierarchyHasher,
            workspaceProvider,
            fileCollectionFactory,
            inputFingerprinter,
            transformFactoryForLegacy,
            scriptCompilationHandler
        );
        return getExecutionEngine(target)
            .createRequest(unitOfWork)
            .execute()
            .getOutputAs(GroovyScriptCompilationOutput.class)
            .get();
    }

    /**
     * We want to use build cache for script compilation, but build cache might not be available yet with early execution engine.
     * Thus settings and init scripts are not using build cache for now.

* * When we compile project build scripts, build cache is available, but we need to query execution engine with build cache support * from the project services directly to use it.

* * TODO: Remove this and just inject execution engine once we unify execution engines in https://github.com/gradle/gradle/issues/27249 */ private ExecutionEngine getExecutionEngine(Object target) { if (target instanceof ProjectInternal) { return ((ProjectInternal) target).getServices().get(ExecutionEngine.class); } return earlyExecutionEngine; } private ClassPath remapClasses(File genericClassesDir, RemappingScriptSource source) { ScriptSource origin = source.getSource(); String className = origin.getClassName(); return classpathTransformer.transform(DefaultClassPath.of(genericClassesDir), new ClassTransform() { @Override public void applyConfigurationTo(Hasher hasher) { hasher.putString(GroovyScriptClassCompiler.class.getSimpleName()); hasher.putInt(1); // transformation version hasher.putString(className); } @Override public Pair apply(ClasspathEntryVisitor.Entry entry, ClassVisitor visitor, ClassData classData) { String renamed = entry.getPath().getLastName(); if (renamed.startsWith(RemappingScriptSource.MAPPED_SCRIPT)) { renamed = className + renamed.substring(RemappingScriptSource.MAPPED_SCRIPT.length()); } byte[] content = classData.getClassContent(); ClassReader cr = new ClassReader(content); String originalClassName = cr.getClassName(); String contentHash = Hashing.hashBytes(content).toString(); BuildScriptRemapper remapper = new BuildScriptRemapper(visitor, origin, originalClassName, contentHash); return Pair.of(entry.getPath().getParent().append(true, renamed), remapper); } }); } @Override public void close() { } static class GroovyScriptCompilationAndInstrumentation extends BuildScriptCompilationAndInstrumentation { private final String templateId; private final HashCode sourceHashCode; private final ClassLoader classLoader; private final ClassLoaderHierarchyHasher classLoaderHierarchyHasher; private final RemappingScriptSource source; private final CompileOperation operation; private final Class scriptBaseClass; private final ScriptCompilationHandler scriptCompilationHandler; private final Action verifier; public GroovyScriptCompilationAndInstrumentation( String templateId, HashCode sourceHashCode, ClassLoader classLoader, RemappingScriptSource source, CompileOperation operation, Action verifier, Class scriptBaseClass, ClassLoaderHierarchyHasher classLoaderHierarchyHasher, ImmutableWorkspaceProvider workspaceProvider, FileCollectionFactory fileCollectionFactory, InputFingerprinter inputFingerprinter, ClasspathElementTransformFactoryForLegacy transformFactoryForLegacy, ScriptCompilationHandler scriptCompilationHandler ) { super(workspaceProvider, fileCollectionFactory, inputFingerprinter, transformFactoryForLegacy); this.templateId = templateId; this.sourceHashCode = sourceHashCode; this.classLoader = classLoader; this.classLoaderHierarchyHasher = classLoaderHierarchyHasher; this.source = source; this.operation = operation; this.verifier = verifier; this.scriptBaseClass = scriptBaseClass; this.scriptCompilationHandler = scriptCompilationHandler; } @Override public Optional shouldDisableCaching(@Nullable OverlappingOutputs detectedOverlappingOutputs) { // Disabled since enabling it introduced negative savings to Groovy script compilation. // It's not disabled for Kotlin since Kotlin has better compile avoidance, additionally // Kotlin has build cache from the beginning and there was no report of a problem with it. return Optional.of(NOT_WORTH_CACHING); } @Override public void visitIdentityInputs(InputVisitor visitor) { visitor.visitInputProperty(TEMPLATE_ID_PROPERTY_NAME, () -> templateId); visitor.visitInputProperty(SOURCE_HASH_PROPERTY_NAME, () -> sourceHashCode); visitor.visitInputProperty(CLASSPATH_PROPERTY_NAME, () -> classLoaderHierarchyHasher.getClassLoaderHash(classLoader)); } @Override public void visitOutputs(File workspace, OutputVisitor visitor) { super.visitOutputs(workspace, visitor); File metadataDir = metadataDir(workspace); OutputFileValueSupplier metadataDirValue = OutputFileValueSupplier.fromStatic(metadataDir, fileCollectionFactory.fixed(metadataDir)); visitor.visitOutputProperty("metadataDir", TreeType.DIRECTORY, metadataDirValue); } @Override public Object loadAlreadyProducedOutput(File workspace) { File instrumentedJar = checkNotNull((File) super.loadAlreadyProducedOutput(workspace)); File metadataDir = metadataDir(workspace); return new GroovyScriptCompilationOutput(instrumentedJar, metadataDir); } @Override public File compile(File workspace) { File classesDir = classesDir(workspace); scriptCompilationHandler.compileToDir(source, classLoader, classesDir, metadataDir(workspace), operation, scriptBaseClass, verifier); return classesDir; } @Override public File instrumentedOutput(File workspace) { return new File(workspace, "instrumented/" + operation.getId()); } private File classesDir(File workspace) { return new File(workspace, "classes/" + operation.getId()); } private static File metadataDir(File workspace) { return new File(workspace, "metadata"); } @Override public String getDisplayName() { return "Groovy DSL script compilation (" + templateId + ")"; } static class GroovyScriptCompilationOutput { private final File instrumentedOutput; private final File metadataDir; public GroovyScriptCompilationOutput(File instrumentedOutput, File metadataDir) { this.instrumentedOutput = instrumentedOutput; this.metadataDir = metadataDir; } public File getInstrumentedOutput() { return instrumentedOutput; } public File getMetadataDir() { return metadataDir; } } } private static class EmptyCompiledScript implements CompiledScript { private final M data; public EmptyCompiledScript(CompileOperation operation) { this.data = operation.getExtractedData(); } @Override public boolean getRunDoesSomething() { return false; } @Override public boolean getHasMethods() { return false; } @Override public void onReuse() { // Ignore } @Override public Class loadClass() { throw new UnsupportedOperationException("Cannot load a script that does nothing."); } @Override public M getData() { return data; } } private static class BuildScriptRemapper extends ClassVisitor implements Opcodes { private static final String SCRIPT_ORIGIN = "org/gradle/internal/scripts/ScriptOrigin"; private final ScriptSource scriptSource; private final String originalClassName; private final String contentHash; public BuildScriptRemapper(ClassVisitor cv, ScriptSource source, String originalClassName, String contentHash) { super(AsmConstants.ASM_LEVEL, cv); this.scriptSource = source; this.originalClassName = originalClassName; this.contentHash = contentHash; } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { String owner = remap(name); boolean shouldAddScriptOrigin = shouldAddScriptOrigin(access); cv.visit(version, access, owner, remap(signature), remap(superName), remapAndAddInterfaces(interfaces, shouldAddScriptOrigin)); if (shouldAddScriptOrigin) { addOriginalClassName(cv, owner, originalClassName); addContentHash(cv, owner, contentHash); } } private static boolean shouldAddScriptOrigin(int access) { return ((access & ACC_INTERFACE) == 0) && ((access & ACC_ANNOTATION) == 0); } private static void addOriginalClassName(ClassVisitor cv, String owner, String originalClassName) { cv.visitField(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC | ACC_FINAL, "__originalClassName", Type.getDescriptor(String.class), "", originalClassName); MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "getOriginalClassName", Type.getMethodDescriptor(Type.getType(String.class)), null, null); mv.visitCode(); mv.visitFieldInsn(GETSTATIC, owner, "__originalClassName", Type.getDescriptor(String.class)); mv.visitInsn(ARETURN); mv.visitMaxs(1, 1); mv.visitEnd(); } private static void addContentHash(ClassVisitor cv, String owner, String contentHash) { cv.visitField(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC | ACC_FINAL, "__signature", Type.getDescriptor(String.class), "", contentHash); MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "getContentHash", Type.getMethodDescriptor(Type.getType(String.class)), null, null); mv.visitCode(); mv.visitFieldInsn(GETSTATIC, owner, "__signature", Type.getDescriptor(String.class)); mv.visitInsn(ARETURN); mv.visitMaxs(1, 1); mv.visitEnd(); } @Override public void visitSource(String source, String debug) { cv.visitSource(scriptSource.getFileName(), debug); } private String[] remapAndAddInterfaces(String[] interfaces, boolean shouldAddScriptOrigin) { if (!shouldAddScriptOrigin) { return remap(interfaces); } if (interfaces == null) { return new String[]{SCRIPT_ORIGIN}; } String[] remapped = new String[interfaces.length + 1]; for (int i = 0; i < interfaces.length; i++) { remapped[i] = remap(interfaces[i]); } remapped[remapped.length - 1] = SCRIPT_ORIGIN; return remapped; } private String[] remap(String[] names) { if (names == null) { return null; } String[] remapped = new String[names.length]; for (int i = 0; i < names.length; i++) { remapped[i] = remap(names[i]); } return remapped; } private String remap(String name) { if (name == null) { return null; } if (RuleVisitor.SOURCE_URI_TOKEN.equals(name)) { URI uri = scriptSource.getResource().getLocation().getURI(); return uri == null ? null : uri.toString(); } if (RuleVisitor.SOURCE_DESC_TOKEN.equals(name)) { return scriptSource.getDisplayName(); } return name.replace(RemappingScriptSource.MAPPED_SCRIPT, scriptSource.getClassName()); } private Object remap(Object o) { if (o instanceof Type) { return Type.getType(remap(((Type) o).getDescriptor())); } if (o instanceof String) { return remap((String) o); } return o; } private Object[] remap(int count, Object[] original) { if (count == 0) { return EMPTY_OBJECT_ARRAY; } Object[] remapped = new Object[count]; for (int idx = 0; idx < count; idx++) { remapped[idx] = remap(original[idx]); } return remapped; } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, remap(desc), remap(signature), remap(exceptions)); if (mv != null && (access & ACC_ABSTRACT) == 0) { mv = new MethodRenamer(mv); } return mv; } @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { return super.visitField(access, name, remap(desc), remap(signature), remap(value)); } @Override public void visitInnerClass(String name, String outerName, String innerName, int access) { super.visitInnerClass(remap(name), remap(outerName), remap(innerName), access); } @Override public void visitOuterClass(String owner, String name, String desc) { super.visitOuterClass(remap(owner), remap(name), remap(desc)); } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return super.visitAnnotation(remap(desc), visible); } class MethodRenamer extends MethodVisitor { public MethodRenamer(final MethodVisitor mv) { super(AsmConstants.ASM_LEVEL, mv); } @Override public void visitTypeInsn(int i, String name) { mv.visitTypeInsn(i, remap(name)); } @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { mv.visitFieldInsn(opcode, remap(owner), name, remap(desc)); } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean intf) { mv.visitMethodInsn(opcode, remap(owner), name, remap(desc), intf); } @Override public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { for (int i = 0; i < bootstrapMethodArguments.length; i++) { bootstrapMethodArguments[i] = remapIfHandle(bootstrapMethodArguments[i]); } mv.visitInvokeDynamicInsn(remap(name), remap(descriptor), remapHandle(bootstrapMethodHandle), bootstrapMethodArguments); } private Object remapIfHandle(Object bootstrapArgument) { if (bootstrapArgument instanceof Handle) { Handle handle = (Handle) bootstrapArgument; return remapHandle(handle); } return bootstrapArgument; } private Handle remapHandle(Handle handle) { return new Handle(handle.getTag(), remap(handle.getOwner()), handle.getName(), remap(handle.getDesc()), handle.isInterface() ); } @Override public void visitLdcInsn(Object cst) { super.visitLdcInsn(remap(cst)); } @Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { super.visitLocalVariable(name, remap(desc), remap(signature), start, end, index); } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return super.visitAnnotation(remap(desc), visible); } @Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { super.visitFrame(type, nLocal, remap(nLocal, local), nStack, remap(nStack, stack)); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy