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

org.gradle.api.internal.runtimeshaded.RuntimeShadedJarCreatorTest.groovy Maven / Gradle / Ivy

There is a newer version: 8.11.1
Show newest version
/*
 * Copyright 2016 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.runtimeshaded

import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import org.apache.ivy.core.settings.IvySettings
import org.cyberneko.html.xercesbridge.XercesBridge
import org.gradle.api.Action
import org.gradle.api.internal.file.collections.DefaultDirectoryFileTreeFactory
import org.gradle.internal.IoActions
import org.gradle.internal.installation.GradleRuntimeShadedJarDetector
import org.gradle.internal.logging.progress.ProgressLoggerFactory
import org.gradle.test.fixtures.file.CleanupTestDirectory
import org.gradle.test.fixtures.file.TestFile
import org.gradle.test.fixtures.file.TestNameTestDirectoryProvider
import org.gradle.util.UsesNativeServices
import org.junit.Rule
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.util.TraceClassVisitor
import spock.lang.Specification

import java.util.jar.JarEntry
import java.util.jar.JarFile

@UsesNativeServices
@CleanupTestDirectory(fieldName = "tmpDir")
class RuntimeShadedJarCreatorTest extends Specification {

    @Rule
    TestNameTestDirectoryProvider tmpDir = new TestNameTestDirectoryProvider()

    def progressLoggerFactory = Stub(ProgressLoggerFactory)
    def relocatedJarCreator
    def outputJar = tmpDir.testDirectory.file('gradle-api.jar')

    def setup() {
        relocatedJarCreator = new RuntimeShadedJarCreator(progressLoggerFactory, new ImplementationDependencyRelocator(RuntimeShadedJarType.API), new DefaultDirectoryFileTreeFactory())
    }

    def "creates JAR file for input directory"() {
        given:
        def inputFilesDir = tmpDir.createDir('inputFiles')
        writeClass(inputFilesDir, "org/gradle/MyClass")

        when:
        relocatedJarCreator.create(outputJar, [inputFilesDir])

        then:
        TestFile[] contents = tmpDir.testDirectory.listFiles().findAll { it.isFile() }
        contents.length == 1
        contents[0] == outputJar
    }

    def "creates fat JAR file for multiple input JAR files"() {
        given:
        def className = 'org/gradle/MyClass'
        def inputFilesDir = tmpDir.createDir('inputFiles')
        def jarFile1 = inputFilesDir.file('lib1.jar')
        createJarFileWithClassFiles(jarFile1, [className])
        def jarFile2 = inputFilesDir.file('lib2.jar')
        createJarFileWithClassFiles(jarFile2, [className])

        when:
        relocatedJarCreator.create(outputJar, [jarFile1, jarFile2])

        then:
        TestFile[] contents = tmpDir.testDirectory.listFiles().findAll { it.isFile() }
        contents.length == 1
        contents[0] == outputJar
    }

    def "creates a reproducible jar"() {
        given:

        def inputFilesDir = tmpDir.createDir('inputFiles')
        def jarFile1 = inputFilesDir.file('lib1.jar')
        createJarFileWithClassFiles(jarFile1, ['org/gradle/MyClass'])
        def jarFile2 = inputFilesDir.file('lib2.jar')
        createJarFileWithClassFiles(jarFile2, ['org/gradle/MySecondClass'])
        def jarFile3 = inputFilesDir.file('lib3.jar')
        def serviceType = 'org.gradle.internal.service.scopes.PluginServiceRegistry'
        createJarFileWithProviderConfigurationFile(jarFile3, serviceType, 'org.gradle.api.internal.artifacts.DependencyServices')
        def jarFile4 = inputFilesDir.file('lib4.jar')
        createJarFileWithProviderConfigurationFile(jarFile4, serviceType, """
# This is some same file
# Ignore comment
org.gradle.api.internal.tasks.CompileServices
# Too many comments""")
        def jarFile5 = inputFilesDir.file('lib5.jar')
        createJarFileWithResources(jarFile5, [
            'org/gradle/reporting/report.js',
            'net/rubygrapefruit/platform/osx-i386/libnative-platform.dylib',
            'aQute/libg/tuple/packageinfo',
            'org/joda/time/tz/data/Africa/Abidjan'])
        def jarFile6 = inputFilesDir.file('lib6.jar')
        createJarFileWithProviderConfigurationFile(jarFile6, 'org.gradle.internal.other.Service', 'org.gradle.internal.other.ServiceImpl')
        def inputDirectory = inputFilesDir.createDir('dir1')
        writeClass(inputDirectory, "org/gradle/MyFirstClass")
        writeClass(inputDirectory, "org/gradle/MyAClass")
        writeClass(inputDirectory, "org/gradle/MyBClass")

        when:
        relocatedJarCreator.create(outputJar, [jarFile1, jarFile2, jarFile3, jarFile4, jarFile5, jarFile6, inputDirectory])

        then:

        TestFile[] contents = tmpDir.testDirectory.listFiles().findAll { it.isFile() }
        contents.length == 1
        contents[0] == outputJar
        handleAsJarFile(outputJar) { JarFile file ->
            List entries = file.entries() as List
            assert entries*.name == [
                'org/gradle/MyClass.class',
                'org/gradle/MySecondClass.class',
                'aQute/libg/tuple/packageinfo',
                'org/gradle/internal/impldep/aQute/libg/tuple/packageinfo',
                'net/rubygrapefruit/platform/osx-i386/libnative-platform.dylib',
                'org/gradle/reporting/report.js',
                'org/joda/time/tz/data/Africa/Abidjan',
                'org/gradle/internal/impldep/org/joda/time/tz/data/Africa/Abidjan',
                'org/gradle/MyAClass.class',
                'org/gradle/MyBClass.class',
                'org/gradle/MyFirstClass.class',
                'META-INF/services/org.gradle.internal.service.scopes.PluginServiceRegistry',
                'META-INF/services/org.gradle.internal.other.Service',
                'META-INF/.gradle-runtime-shaded']
        }
        outputJar.md5Hash == "8eb7b9c992e83362a1445585b00a4fd0"
    }

    def "excludes module-info.class from jar"() {
        given:

        def inputFilesDir = tmpDir.createDir('inputFiles')
        def jarFile1 = inputFilesDir.file('lib1.jar')
        createJarFileWithClassFiles(jarFile1, ['org/gradle/MyClass'])
        def jarFile2 = inputFilesDir.file('lib2.jar')
        createJarFileWithClassFiles(jarFile2, ['org/gradle/MySecondClass', 'module-info'])
        def inputDirectory = inputFilesDir.createDir('dir1')
        writeClass(inputDirectory, "org/gradle/MyFirstClass")

        when:
        relocatedJarCreator.create(outputJar, [jarFile1, jarFile2, inputDirectory])

        then:

        TestFile[] contents = tmpDir.testDirectory.listFiles().findAll { it.isFile() }
        contents.length == 1
        contents[0] == outputJar
        handleAsJarFile(outputJar) { JarFile file ->
            List entries = file.entries() as List
            assert entries*.name == [
                'org/gradle/MyClass.class',
                'org/gradle/MySecondClass.class',
                'org/gradle/MyFirstClass.class',
                'META-INF/.gradle-runtime-shaded']
        }
    }

    def "merges provider-configuration file with the same name"() {
        given:
        def inputFilesDir = tmpDir.createDir('inputFiles')
        def serviceType = 'org.gradle.internal.service.scopes.PluginServiceRegistry'
        def jarFile1 = inputFilesDir.file('lib1.jar')
        createJarFileWithProviderConfigurationFile(jarFile1, serviceType, 'org.gradle.api.internal.artifacts.DependencyServices')
        def jarFile2 = inputFilesDir.file('lib2.jar')
        createJarFileWithProviderConfigurationFile(jarFile2, serviceType, """

org.gradle.plugin.use.internal.PluginUsePluginServiceRegistry

""")
        def jarFile3 = inputFilesDir.file('lib3.jar')
        createJarFileWithProviderConfigurationFile(jarFile3, serviceType, """
# This is some same file
# Ignore comment
org.gradle.api.internal.tasks.CompileServices
# Too many comments""")

        when:
        relocatedJarCreator.create(outputJar, [jarFile1, jarFile2, jarFile3])

        then:
        TestFile[] contents = tmpDir.testDirectory.listFiles().findAll { it.isFile() }
        contents.length == 1
        def relocatedJar = contents[0]
        relocatedJar == outputJar

        handleAsJarFile(relocatedJar) { JarFile jar ->
            JarEntry providerConfigJarEntry = jar.getJarEntry("META-INF/services/$serviceType")
            IoActions.withResource(jar.getInputStream(providerConfigJarEntry), new Action() {
                void execute(InputStream inputStream) {
                    assert inputStream.text == """org.gradle.api.internal.artifacts.DependencyServices
org.gradle.plugin.use.internal.PluginUsePluginServiceRegistry
org.gradle.api.internal.tasks.CompileServices"""
                }
            })
        }
    }

    def "relocates Gradle impl dependency classes"() {
        given:
        def noRelocationClassNames = ['org/gradle/MyClass',
                                      'java/lang/String',
                                      'javax/inject/Inject',
                                      'groovy/util/XmlSlurper',
                                      'groovyjarjarantlr/TokenStream',
                                      'net/rubygrapefruit/platform/FileInfo',
                                      'org/codehaus/groovy/ant/Groovyc',
                                      'org/apache/tools/ant/taskdefs/Ant',
                                      'org/slf4j/Logger',
                                      'org/apache/commons/logging/Log',
                                      'org/apache/log4j/Logger',
                                      'org/apache/xerces/parsers/SAXParser',
                                      'org/w3c/dom/Document',
                                      'org/xml/sax/XMLReader']
        def relocationClassNames = ['org/apache/commons/lang/StringUtils',
                                    'com/google/common/collect/Lists']
        def classNames = noRelocationClassNames + relocationClassNames
        def inputFilesDir = tmpDir.createDir('inputFiles')
        def jarFile = inputFilesDir.file('lib.jar')
        createJarFileWithClassFiles(jarFile, classNames)

        when:
        relocatedJarCreator.create(outputJar, [jarFile])

        then:
        TestFile[] contents = tmpDir.testDirectory.listFiles().findAll { it.isFile() }
        contents.length == 1
        def relocatedJar = contents[0]
        relocatedJar == outputJar

        handleAsJarFile(relocatedJar) { JarFile jar ->
            assert jar.getJarEntry('org/gradle/MyClass.class')
            assert jar.getJarEntry('java/lang/String.class')
            assert jar.getJarEntry('javax/inject/Inject.class')
            assert jar.getJarEntry('groovy/util/XmlSlurper.class')
            assert jar.getJarEntry('groovyjarjarantlr/TokenStream.class')
            assert jar.getJarEntry('net/rubygrapefruit/platform/FileInfo.class')
            assert jar.getJarEntry('org/codehaus/groovy/ant/Groovyc.class')
            assert jar.getJarEntry('org/apache/tools/ant/taskdefs/Ant.class')
            assert jar.getJarEntry('org/slf4j/Logger.class')
            assert jar.getJarEntry('org/apache/commons/logging/Log.class')
            assert jar.getJarEntry('org/apache/log4j/Logger.class')
            assert jar.getJarEntry('org/apache/xerces/parsers/SAXParser.class')
            assert jar.getJarEntry('org/w3c/dom/Document.class')
            assert jar.getJarEntry('org/xml/sax/XMLReader.class')
            assert jar.getJarEntry('org/gradle/internal/impldep/org/apache/commons/lang/StringUtils.class')
            assert jar.getJarEntry('org/gradle/internal/impldep/com/google/common/collect/Lists.class')
        }
    }

    def "remaps old-style string class literals"() {
        given:
        def clazz = IvySettings
        byte[] classData = clazz.getClassLoader().getResourceAsStream("${clazz.name.replace('.', '/')}.class").bytes

        when:
        def remapped = relocatedJarCreator.remapClass(clazz.name, classData)
        def cr = new ClassReader(remapped)
        def writer = new StringWriter()
        def tcv = new TraceClassVisitor(new PrintWriter(writer))
        cr.accept(tcv, 0)

        then:
        def bytecode = writer.toString()
        !bytecode.contains('LDC "org.apache.ivy.core.settings.XmlSettingsParser"')
        bytecode.contains('static synthetic Ljava/lang/Class; class$org$gradle$internal$impldep$org$apache$ivy$core$settings$IvySettings')
        bytecode.contains('GETSTATIC org/gradle/internal/impldep/org/apache/ivy/core/settings/IvySettings.class$org$gradle$internal$impldep$org$apache$ivy$core$settings$IvySettings : Ljava/lang/Class;')
        bytecode.contains('LDC "org.gradle.internal.impldep.org.apache.ivy.core.settings.IvySettings"')
    }

    def "remaps class literals in strings"() {
        given:
        def clazz = XercesBridge
        byte[] classData = clazz.getClassLoader().getResourceAsStream("${clazz.name.replace('.', '/')}.class").bytes

        when:
        def remapped = relocatedJarCreator.remapClass(clazz.name, classData)
        def cr = new ClassReader(remapped)
        def writer = new StringWriter()
        def tcv = new TraceClassVisitor(new PrintWriter(writer))
        cr.accept(tcv, 0)

        then:
        def bytecode = writer.toString()
        !bytecode.contains('LDC "org.cyberneko.html.xercesbridge.XercesBridge_2_3"')
        bytecode.contains('LDC "org.gradle.internal.impldep.org.cyberneko.html.xercesbridge.XercesBridge_2_3"')
    }

    def "remaps class literals in strings with slashes"() {
        given:
        def clazz = JavaAdapter
        byte[] classData = clazz.getClassLoader().getResourceAsStream("${clazz.name.replace('.', '/')}.class").bytes

        when:
        def remapped = relocatedJarCreator.remapClass(clazz.name, classData)
        def cr = new ClassReader(remapped)
        def writer = new StringWriter()
        def tcv = new TraceClassVisitor(new PrintWriter(writer))
        cr.accept(tcv, 0)

        then:
        def bytecode = writer.toString()
        !bytecode.contains('LDC "org/mozilla/javascript/Context"')
        bytecode.contains('LDC "org/gradle/internal/impldep/org/mozilla/javascript/Context"')
    }

    def "ignores slf4j logger bindings"() {
        given:
        def inputFilesDir = tmpDir.createDir('inputFiles')
        def jarFile = inputFilesDir.file('lib.jar')
        createJarFileWithClassFiles(jarFile, ["org.slf4j.impl.StaticLoggerBinder"])

        when:
        relocatedJarCreator.create(outputJar, [jarFile])

        then:
        handleAsJarFile(outputJar) {
            it.getEntry("org/slf4j/impl/StaticLoggerBinder.class")
        }
    }

    def "remaps resources"() {
        given:
        def noRelocationResources = ['org/gradle/reporting/report.js',
                                     'net/rubygrapefruit/platform/osx-i386/libnative-platform.dylib']
        def duplicateResources = ['aQute/libg/tuple/packageinfo',
                                  'org/joda/time/tz/data/Africa/Abidjan']
        def onlyRelocatedResources = [] // None
        def generatedFiles = [GradleRuntimeShadedJarDetector.MARKER_FILENAME]
        def resources = noRelocationResources + duplicateResources + onlyRelocatedResources
        def inputFilesDir = tmpDir.createDir('inputFiles')
        def jarFile = inputFilesDir.file('lib.jar')
        createJarFileWithResources(jarFile, resources)

        when:
        relocatedJarCreator.create(outputJar, [jarFile])

        then:
        TestFile[] contents = tmpDir.testDirectory.listFiles().findAll { it.isFile() }
        contents.length == 1
        def relocatedJar = contents[0]
        relocatedJar == outputJar

        handleAsJarFile(relocatedJar) { JarFile jar ->
            assert jar.entries().toList().size() ==
                noRelocationResources.size() +
                duplicateResources.size() * 2 +
                onlyRelocatedResources.size() +
                generatedFiles.size()
            noRelocationResources.each { resourceName ->
                assert jar.getEntry(resourceName)
            }
            duplicateResources.each { resourceName ->
                assert jar.getEntry(resourceName)
                assert jar.getEntry("org/gradle/internal/impldep/$resourceName")
            }
            onlyRelocatedResources.each { resourceName ->
                assert jar.getEntry("org/gradle/internal/impldep/$resourceName")
            }
            generatedFiles.each { resourceName ->
                assert jar.getEntry(resourceName)
            }
        }
    }

    private void createJarFileWithClassFiles(TestFile jar, List classNames) {
        TestFile contents = tmpDir.createDir("contents/$jar.name")

        classNames.each { className ->
            writeClass(contents, className)
        }

        contents.zipTo(jar)
    }

    private static void writeClass(TestFile outputDir, String className) {
        TestFile classFile = outputDir.createFile("${className}.class")
        ClassNode classNode = new ClassNode()
        classNode.version = className=='module-info'?Opcodes.V9:Opcodes.V1_6
        classNode.access = Opcodes.ACC_PUBLIC
        classNode.name = className
        classNode.superName = 'java/lang/Object'

        ClassWriter cw = new ClassWriter(0)
        classNode.accept(cw)

        classFile.withOutputStream {
            it.write(cw.toByteArray())
        }
    }

    private void createJarFileWithProviderConfigurationFile(TestFile jar, String serviceType, String serviceProvider) {
        TestFile contents = tmpDir.createDir("contents/$jar.name")
        contents.createFile("META-INF/services/$serviceType") << serviceProvider
        contents.zipTo(jar)
    }

    private void createJarFileWithResources(TestFile jar, List resourceNames) {
        TestFile contents = tmpDir.createDir("contents/$jar.name")
        resourceNames.each { resourceName ->
            contents.createFile(resourceName) << resourceName
        }
        contents.zipTo(jar)
    }

    static void handleAsJarFile(File jar, @ClosureParams(value = SimpleType, options = ["java.util.jar.JarFile"]) Closure c) {
        new JarFile(jar).withCloseable(c)
    }

    static class JavaAdapter {
        // emulates what is found in org.mozilla.javascript.JavaAdapter
        // (we can't use it directly because not found on test classpath)
        void foo() {
            String[] classes = ['org/mozilla/javascript/Context']
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy