Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 82 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -179,19 +179,38 @@ dependencies {
}

shadowJar {
minimize()
// Keep annotation packages that Guava classfiles still reference. Without them,
// consumers compiling with -Xlint:classfile -Werror fail (see #4436): the shaded
// ImmutableList etc. retain RuntimeInvisibleAnnotations pointing at
// com.google.common.annotations / errorprone / j2objc / checker / jsr305 types
// that were never shipped in the published jar.
minimize {
exclude(dependency('com.google.errorprone:error_prone_annotations:.*'))
exclude(dependency('com.google.j2objc:j2objc-annotations:.*'))
exclude(dependency('org.checkerframework:checker-qual:.*'))
exclude(dependency('com.google.code.findbugs:jsr305:.*'))
}
archiveClassifier.set('')
configurations = [project.configurations.compileClasspath]
relocate('com.google.common', 'graphql.com.google.common') {
include 'com.google.common.collect.*'
include 'com.google.common.base.*'
include 'com.google.common.math.*'
include 'com.google.common.primitives.*'
include 'com.google.common.annotations.*'
}
relocate('com.google.errorprone.annotations', 'graphql.com.google.errorprone.annotations')
relocate('com.google.j2objc.annotations', 'graphql.com.google.j2objc.annotations')
relocate('org.checkerframework', 'graphql.org.checkerframework')
relocate('javax.annotation', 'graphql.javax.annotation')
relocate('org.antlr.v4.runtime', 'graphql.org.antlr.v4.runtime')
dependencies {
include(dependency('com.google.guava:guava:' + guavaVersion))
include(dependency('org.antlr:antlr4-runtime:' + antlrVersion))
include(dependency('com.google.errorprone:error_prone_annotations:.*'))
include(dependency('com.google.j2objc:j2objc-annotations:.*'))
include(dependency('org.checkerframework:checker-qual:.*'))
include(dependency('com.google.code.findbugs:jsr305:.*'))
}
from "LICENSE.md"
from "src/main/antlr/Graphql.g4"
Expand Down Expand Up @@ -220,7 +239,7 @@ tasks.named('shadowJar').configure {
bundle.bnd('''
-exportcontents: graphql.*
-removeheaders: Private-Package
Import-Package: !android.os.*,!com.google.*,!org.checkerframework.*,!graphql.com.google.*,!org.antlr.*,!graphql.org.antlr.*,!sun.misc.*,org.jspecify.annotations;resolution:=optional,*
Import-Package: !android.os.*,!com.google.*,!org.checkerframework.*,!graphql.com.google.*,!graphql.org.checkerframework.*,!graphql.javax.*,!org.antlr.*,!graphql.org.antlr.*,!sun.misc.*,org.jspecify.annotations;resolution:=optional,*
''')
}
}
Expand Down Expand Up @@ -311,6 +330,64 @@ buildNewJar.dependsOn extractWithoutGuava

shadowJar.finalizedBy extractWithoutGuava, buildNewJar

// Fail the build if the published jar still embeds Guava classfiles that point at
// annotation types not present in the jar (javac -Xlint:classfile -Werror for consumers).
tasks.register('verifyShadedJarAnnotationRefs') {
group = 'verification'
description = 'Ensure shaded Guava classfiles do not reference missing annotation types (#4436)'
dependsOn buildNewJar
def jarFile = layout.buildDirectory.file("libs/graphql-java-${project.version}.jar")
inputs.file(jarFile)
outputs.upToDateWhen { false }
doLast {
def jar = jarFile.get().asFile
if (!jar.exists()) {
throw new GradleException("Expected published jar at ${jar}")
}
def missing = new LinkedHashSet<String>()
def present = new HashSet<String>()
// Match both pre-relocate and post-relocate annotation descriptors in constant pools.
def annotationDesc = ~/L(graphql\/)?((?:com\/google\/(?:common\/annotations|errorprone\/annotations|j2objc\/annotations)|org\/checkerframework|javax\/annotation)[^;]*);/
java.util.zip.ZipFile zip = new java.util.zip.ZipFile(jar)
try {
zip.entries().each { entry ->
if (entry.name.endsWith('.class')) {
present.add(entry.name.substring(0, entry.name.length() - 6).replace('/', '.'))
}
}
zip.entries().each { entry ->
if (!entry.name.startsWith('graphql/com/google/') || !entry.name.endsWith('.class')) {
return
}
def bytes = zip.getInputStream(entry).bytes
def text = new String(bytes, 'ISO-8859-1')
def matcher = annotationDesc.matcher(text)
while (matcher.find()) {
def relocatedPrefix = matcher.group(1)
def typePath = matcher.group(2).replace('/', '.')
if (relocatedPrefix == null) {
missing.add("${entry.name} -> ${typePath} (not relocated)")
continue
}
def typeName = 'graphql.' + typePath
if (!present.contains(typeName)) {
missing.add("${entry.name} -> ${typeName} (class missing from jar)")
}
}
}
} finally {
zip.close()
}
if (!missing.isEmpty()) {
def msg = new StringBuilder('Shaded Guava classfiles reference annotation types that are missing or not relocated:\n')
missing.each { msg.append(" - ${it}\n") }
throw new GradleException(msg.toString())
}
}
}
buildNewJar.finalizedBy verifyShadedJarAnnotationRefs
tasks.named('check') { dependsOn verifyShadedJarAnnotationRefs }


// --- TestNG TCK skip verification ---
// The Reactive Streams TCK PublisherVerification base class silently converts optional test
Expand Down Expand Up @@ -582,7 +659,9 @@ jacocoTestReport {
fileTree(dir: layout.buildDirectory.dir('classes-jacoco/java/main'), exclude: [
'graphql/parser/antlr/**',
'graphql/com/google/**',
'graphql/org/antlr/**'
'graphql/org/antlr/**',
'graphql/org/checkerframework/**',
'graphql/javax/**'
])
))
}
Expand Down
77 changes: 77 additions & 0 deletions src/test/groovy/graphql/ShadedJarAnnotationRefsTest.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package graphql

import spock.lang.IgnoreIf
import spock.lang.Specification

import java.util.jar.JarFile
import java.util.regex.Pattern

/**
* Guards #4436: published jar must not ship shaded Guava classfiles whose
* RuntimeInvisibleAnnotations still point at annotation types that are absent
* from the jar (breaks consumers compiling with -Xlint:classfile -Werror).
*
* Runs only when the published jar has already been built (shadowJar/buildNewJar).
*/
class ShadedJarAnnotationRefsTest extends Specification {

private static final Pattern ANNOTATION_DESC = Pattern.compile(
'L(graphql/)?((?:com/google/(?:common/annotations|errorprone/annotations|j2objc/annotations)|org/checkerframework|javax/annotation)[^;]*);'
)

private static File findPublishedJar() {
def libs = new File("build/libs")
if (!libs.directory) {
return null
}
def jars = libs.listFiles({ dir, name ->
name.startsWith("graphql-java-") && name.endsWith(".jar") &&
!name.contains("sources") && !name.contains("javadoc") &&
!name.contains("tmp")
} as FilenameFilter)
if (jars == null || jars.length == 0) {
return null
}
return jars.toList().sort { -it.lastModified() }.first()
}

@IgnoreIf({ ShadedJarAnnotationRefsTest.findPublishedJar() == null })
def "shaded Guava classes do not reference missing annotation types"() {
given:
def jar = findPublishedJar()
def present = new HashSet<String>()
def problems = new LinkedHashSet<String>()

when:
new JarFile(jar).withCloseable { jarFile ->
jarFile.entries().each { entry ->
if (entry.name.endsWith(".class")) {
present.add(entry.name.substring(0, entry.name.length() - 6).replace('/', '.'))
}
}
jarFile.entries().each { entry ->
if (!entry.name.startsWith("graphql/com/google/") || !entry.name.endsWith(".class")) {
return
}
def bytes = jarFile.getInputStream(entry).bytes
def text = new String(bytes, "ISO-8859-1")
def matcher = ANNOTATION_DESC.matcher(text)
while (matcher.find()) {
def relocatedPrefix = matcher.group(1)
def typePath = matcher.group(2).replace('/', '.')
if (relocatedPrefix == null) {
problems.add("${entry.name} -> ${typePath} (not relocated)")
continue
}
def typeName = "graphql." + typePath
if (!present.contains(typeName)) {
problems.add("${entry.name} -> ${typeName} (class missing from jar)")
}
}
}
}

then:
problems.isEmpty()
}
}