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
58 changes: 37 additions & 21 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,19 @@ switch (JavaVersion.current()) {
break;
}

task setLocalRepo(type:Exec) {
commandLine 'git', 'worktree', 'list'
standardOutput = new ByteArrayOutputStream()
task setLocalRepo(type: Exec) {
def outputFile = layout.buildDirectory.file("setLocalRepo-output.txt")
outputs.file(outputFile)

commandLine = [
'bash',
'-c',
"git worktree list > ${outputFile.get().asFile.absolutePath}"
]

doLast {
String worktreeList = standardOutput.toString()
localRepo = worktreeList.substring(0, worktreeList.indexOf(' ')) + '/.git'
String worktreeList = outputFile.get().asFile.text
ext.localRepo = worktreeList.substring(0, worktreeList.indexOf(' ')) + '/.git'
}
}

Expand All @@ -146,7 +153,7 @@ spotless {
// > Could not create task ':checker-qual:spotlessJava'.
// > File signature can only be created for existing regular files, given:
// .../checker-framework/checker-qual/build/libs/checker-qual-3.25.1-SNAPSHOT.jar
predeclareDepsFromBuildscript()
predeclareDeps()
}

spotlessPredeclare {
Expand Down Expand Up @@ -1004,31 +1011,40 @@ subprojects {
withSourcesJar()
}

// Capture project references early.
def projectVersion = project.version
def projectName = project.name.replaceAll('-', '.')

// Things in this block reference definitions in the subproject that do not exist,
// until the project is evaluated.
afterEvaluate {
// Adds manifest to all Jar files
tasks.withType(Jar) {
includeEmptyDirs = false
if (archiveFileName.get().startsWith('checker-qual') || archiveFileName.get().startsWith('checker-util')) {
metaInf {
from './LICENSE.txt'
}
} else {
metaInf {
from "${rootDir}/LICENSE.txt"

archiveFileName.map { fileName ->
if (fileName.startsWith('checker-qual') || fileName.startsWith('checker-util')) {
metaInf {
from './LICENSE.txt'
}
} else {
metaInf {
from "${rootDir}/LICENSE.txt"
}
}
}
manifest {
attributes('Implementation-Version': "${project.version}")
attributes('Implementation-Version': "${projectVersion}")
attributes('Implementation-URL': 'https://eisop.github.io/')
if (! archiveFileName.get().endsWith('source.jar') && ! archiveFileName.get().startsWith('checker-qual')) {
attributes('Automatic-Module-Name': 'org.checkerframework.' + project.name.replaceAll('-', '.'))
}
if (archiveFileName.get().startsWith('checker-qual') || archiveFileName.get().startsWith('checker-util')) {
attributes('Bundle-License': 'MIT')
} else {
attributes('Bundle-License': '(GPL-2.0-only WITH Classpath-exception-2.0)')
archiveFileName.map { fileName ->
if (! fileName.endsWith('source.jar') && ! fileName.startsWith('checker-qual')) {
attributes('Automatic-Module-Name': 'org.checkerframework.' + projectName)
}
if (fileName.startsWith('checker-qual') || fileName.startsWith('checker-util')) {
attributes('Bundle-License': 'MIT')
} else {
attributes('Bundle-License': '(GPL-2.0-only WITH Classpath-exception-2.0)')
}
}
}
}
Expand Down
10 changes: 6 additions & 4 deletions checker-qual-android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
evaluationDependsOn(':checker-qual')

tasks.register('deletePreviousSources') {
// Delete the directory in case a previously copied file should no longer be in checker-qual.
delete file(layout.buildDirectory.dir("generated/sources/main/java"))
Comment on lines +3 to +5

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deletePreviousSources task performs a delete operation during the configuration phase (line 5), which violates configuration cache principles. The delete operation should be moved to the execution phase by wrapping it in a doFirst or doLast block, or by making this a Delete task type instead of a generic task.

Suggested change
tasks.register('deletePreviousSources') {
// Delete the directory in case a previously copied file should no longer be in checker-qual.
delete file(layout.buildDirectory.dir("generated/sources/main/java"))
tasks.register('deletePreviousSources', Delete) {
// Delete the directory in case a previously copied file should no longer be in checker-qual.
delete layout.buildDirectory.dir("generated/sources/main/java")

Copilot uses AI. Check for mistakes.
}

task copySources(type: Copy) {
description = 'Copy checker-qual source to checker-qual-android'
dependsOn 'deletePreviousSources'
includeEmptyDirs = false
doFirst {
// Delete the directory in case a previously copied file should no longer be in checker-qual
delete file(layout.buildDirectory.dir("generated/sources/main/java"))
}
from files(project(':checker-qual').sourceSets.main.java)
include '**/*.java'
exclude '**/SignednessUtilExtra.java'
Expand Down
4 changes: 4 additions & 0 deletions checker-qual/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ jar {
manifest {
attributes('Export-Package': '*')
}
// Work around http://github.com/bndtools/bnd/issues/6346
bundle {
properties.put("project.group", provider({project.group}))
}
}

apply from: rootProject.file('gradle-mvn-push.gradle')
Expand Down
107 changes: 56 additions & 51 deletions checker/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -133,45 +133,43 @@ jar {
// This task differs from the `assemble` task in that it does not build Javadoc.
// It is useful for those who only want to run `javac`.
// checker.jar is copied to checker/dist/ when it is built by the shadowJar task.
task assembleForJavac(dependsOn: shadowJar, group: 'Build') {
task assembleForJavac(group: 'Build') {
description = 'Builds or downloads jars required by CheckerMain and puts them in checker/dist/.'
dependsOn 'copyCheckerQualJar'
dependsOn 'copyCheckerUtilJar'
dependsOn 'copyJavacJar'
}

tasks.register('copyCheckerQualJar', Copy) {
dependsOn ':checker-qual:jar'

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copyCheckerQualJar task has duplicate dependsOn declarations (lines 144-145). Both dependsOn ':checker-qual:jar' and dependsOn project(':checker-qual').tasks.jar refer to the same task. One of these should be removed to avoid redundancy.

Suggested change
dependsOn ':checker-qual:jar'

Copilot uses AI. Check for mistakes.
dependsOn project(':checker-qual').tasks.jar
def checkerQualJarFile = file(project(':checker-qual').tasks.getByName('jar').archiveFile)
def checkerUtilJarFile = file(project(':checker-util').tasks.getByName('jar').archiveFile)

doLast {
if (!checkerQualJarFile.exists()) {
throw new GradleException('File not found: ' + checkerQualJarFile)
}
copy {
from checkerQualJarFile
into "${projectDir}/dist"
rename { String fileName ->
// remove version number on checker-qual.jar
fileName.replace(fileName, 'checker-qual.jar')
}
}
from project(':checker-qual').tasks.jar.archiveFile
into "${projectDir}/dist"
rename { String fileName ->
// remove version number on checker-qual.jar
fileName.replace(fileName, 'checker-qual.jar')
}
}

if (!checkerUtilJarFile.exists()) {
throw new GradleException('File not found: ' + checkerUtilJarFile)
}
copy {
from checkerUtilJarFile
into "${projectDir}/dist"
rename { String fileName ->
// remove version number on checker-util.jar
fileName.replace(fileName, 'checker-util.jar')
}
}
tasks.register('copyCheckerUtilJar', Copy) {
dependsOn 'shadowJar'
dependsOn project(':checker-util').tasks.jar

copy {
// This is required to *run* the Checker Framework on JDK 8.
from configurations.javacJar
into "${projectDir}/dist"
rename { String fileName ->
fileName.replace(fileName, 'javac.jar')
}
}
from project(':checker-util').tasks.jar.archiveFile
into "${projectDir}/dist"
rename { String fileName ->
// remove version number on checker-util.jar
fileName.replace(fileName, 'checker-util.jar')
}
}

tasks.register('copyJavacJar', Copy) {
// This is required to *run* the Checker Framework on JDK 8.
from configurations.javacJar
into "${projectDir}/dist"
rename { String fileName ->
fileName.replace(fileName, 'javac.jar')
}
}

Expand Down Expand Up @@ -234,17 +232,22 @@ shadowJar {
// To see what files are incorporated into the shadow jar file:
// doFirst { println sourceSets.main.runtimeClasspath.asPath }
archiveClassifier = 'all'
doLast{
copy {
from archiveFile.get()
into file("${projectDir}/dist")
rename 'checker.*', 'checker.jar'
}
}

minimize()
}

shadowJar.finalizedBy('copyCheckerJar')

tasks.register('copyCheckerJar', Copy) {
dependsOn ':checker:shadowJar'

// This used `from archiveFile.get()` when it was part of shadowJar.
// Look at e.g. copyCheckerQualJar whether there is a better way to do this.
from file("${projectDir}/build/libs/checker-${project.version}-all.jar")
into file("${projectDir}/dist")
rename 'checker.*', 'checker.jar'
}

artifacts {
// Don't add this here or else the Javadoc and the sources jar is built during the assemble task.
// archives allJavadocJar
Expand All @@ -257,8 +260,12 @@ artifacts {

clean {
def injected = project.objects.newInstance(InjectedExecOps)
def bldDir = buildDir
def distDir = "${projectDir}/dist"
def cmdlineDir = file('tests/command-line')
def nullnessextraDir = file('tests/nullness-extra')

delete("${projectDir}/dist")
delete(distDir)
delete('tests/calledmethods-delomboked')
delete('tests/ainfer-testchecker/annotated')
delete('tests/ainfer-testchecker/inference-output')
Expand All @@ -271,20 +278,18 @@ clean {
delete('tests/build')
doLast {
injected.execOps.exec {
workingDir = file('tests/command-line')
workingDir = cmdlineDir
commandLine 'make', 'clean'
}
injected.execOps.exec {
workingDir = file('tests/nullness-extra')
workingDir = nullnessextraDir
commandLine 'make', 'clean'
}
}
}

clean.doLast {
while (buildDir.exists()) {
sleep(10000) // wait 10 seconds
buildDir.deleteDir()
while(bldDir.exists()) {
sleep(10000) // wait 10 seconds
bldDir.deleteDir()
}
}
}

Expand Down Expand Up @@ -1090,7 +1095,7 @@ final checkerPom(publication) {
publishing {
publications {
checker(MavenPublication) {
project.shadow.component it
from components.shadow
checkerPom it
artifact checkerJar
artifact allSourcesJar
Expand Down
3 changes: 2 additions & 1 deletion docs/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ interface InjectedExecOps {

clean {
def injected = project.objects.newInstance(InjectedExecOps)
def examples = file('examples')

delete('api/')
doLast {
injected.execOps.exec {
workingDir = file('examples')
workingDir = examples
commandLine 'make', 'clean'
}
}
Expand Down
1 change: 1 addition & 0 deletions docs/examples/errorprone/gradle.properties
1 change: 1 addition & 0 deletions docs/examples/jspecify/gradle.properties
1 change: 1 addition & 0 deletions docs/examples/lombok/gradle.properties
1 change: 1 addition & 0 deletions docs/examples/nullaway/gradle.properties
72 changes: 40 additions & 32 deletions framework/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,52 @@ tasks.register('cloneAnnotatedJdk', CloneTask) {
outputs.upToDateWhen { false }
}

task copyAndMinimizeAnnotatedJdkFiles(type: JavaExec, dependsOn: cloneAnnotatedJdk, group: 'Build') {
tasks.register('copyAnnotatedJdkFiles', Copy) {
dependsOn ':framework:cloneAnnotatedJdk'

def inputDir = "${annotatedJdkHome}/src"
def outputDir = "${buildDir}/generated/resources/annotated-jdk/"

inputs.dir file(inputDir)
outputs.dir file(outputDir)

FileTree tree = fileTree(dir: inputDir)
String absolutejdkHome = file(annotatedJdkHome).absolutePath
int jdkDirStringSize = absolutejdkHome.size()

NavigableSet<String> annotatedForFiles = new TreeSet<>();
// Populate `annotatedForFiles`.
tree.visit { FileVisitDetails fvd ->
if (!fvd.file.isDirectory() && fvd.file.name.matches('.*\\.java')
&& !fvd.file.path.contains('org/checkerframework')) {
fvd.getFile().readLines().any { line ->
if (line.contains('@AnnotatedFor') || line.contains('org.checkerframework') || line.contains('org.jspecify')) {
annotatedForFiles.add(fvd.file.absolutePath)
return true;
}
}
}
}

from(annotatedJdkHome)
into(outputDir)
for (String filename : annotatedForFiles) {
include filename.substring(jdkDirStringSize)
Comment on lines +109 to +130

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copyAnnotatedJdkFiles task has configuration cache compatibility issues. The task is reading files and populating the annotatedForFiles set during the configuration phase (lines 109-125), which violates configuration cache principles. This work should be moved to the execution phase using a doFirst or doLast block. Additionally, the file tree traversal and file reading should be done at execution time to avoid capturing file system state during configuration.

Suggested change
FileTree tree = fileTree(dir: inputDir)
String absolutejdkHome = file(annotatedJdkHome).absolutePath
int jdkDirStringSize = absolutejdkHome.size()
NavigableSet<String> annotatedForFiles = new TreeSet<>();
// Populate `annotatedForFiles`.
tree.visit { FileVisitDetails fvd ->
if (!fvd.file.isDirectory() && fvd.file.name.matches('.*\\.java')
&& !fvd.file.path.contains('org/checkerframework')) {
fvd.getFile().readLines().any { line ->
if (line.contains('@AnnotatedFor') || line.contains('org.checkerframework') || line.contains('org.jspecify')) {
annotatedForFiles.add(fvd.file.absolutePath)
return true;
}
}
}
}
from(annotatedJdkHome)
into(outputDir)
for (String filename : annotatedForFiles) {
include filename.substring(jdkDirStringSize)
from(annotatedJdkHome)
into(outputDir)
doFirst {
String absolutejdkHome = file(annotatedJdkHome).absolutePath
int jdkDirStringSize = absolutejdkHome.size()
NavigableSet<String> annotatedForFiles = new TreeSet<>()
FileTree tree = fileTree(dir: inputDir)
// Populate `annotatedForFiles`.
tree.visit { FileVisitDetails fvd ->
if (!fvd.file.isDirectory() && fvd.file.name.matches('.*\\.java')
&& !fvd.file.path.contains('org/checkerframework')) {
fvd.getFile().readLines().any { line ->
if (line.contains('@AnnotatedFor') || line.contains('org.checkerframework') || line.contains('org.jspecify')) {
annotatedForFiles.add(fvd.file.absolutePath)
return true
}
}
}
}
for (String filename : annotatedForFiles) {
include filename.substring(jdkDirStringSize)
}

Copilot uses AI. Check for mistakes.
}
}

task copyAndMinimizeAnnotatedJdkFiles(type: JavaExec, dependsOn: copyAnnotatedJdkFiles, group: 'Build') {
def outputDir = "${buildDir}/generated/resources/annotated-jdk/"

description = "Copy annotated JDK files to ${outputDir}. Removes private and package-private methods, method bodies, comments, etc. from the annotated JDK"

dependsOn ':framework:compileStubifierJava'
// we need the next two dependencies because we run JavaStubifier using this project's runtimeClasspath,
// which refers to the jars for these other projects
dependsOn ':javacutil:jar'
dependsOn ':dataflow:jar'
def inputDir = "${annotatedJdkHome}/src"
def outputDir = "${buildDir}/generated/resources/annotated-jdk/"

description = "Copy annotated JDK files to ${outputDir}. Removes private and package-private methods, method bodies, comments, etc. from the annotated JDK"

inputs.dir file(inputDir)
inputs.dir file(outputDir)
outputs.dir file(outputDir)

classpath = sourceSets.stubifier.runtimeClasspath
Expand All @@ -117,32 +151,6 @@ task copyAndMinimizeAnnotatedJdkFiles(type: JavaExec, dependsOn: cloneAnnotatedJ

mainClass = 'org.checkerframework.framework.stubifier.JavaStubifier'
args outputDir

doFirst {
FileTree tree = fileTree(dir: inputDir)
NavigableSet<String> annotatedForFiles = new TreeSet<>();
// Populate `annotatedForFiles`.
tree.visit { FileVisitDetails fvd ->
if (!fvd.file.isDirectory() && fvd.file.name.matches('.*\\.java')
&& !fvd.file.path.contains('org/checkerframework')) {
fvd.getFile().readLines().any { line ->
if (line.contains('@AnnotatedFor') || line.contains('org.checkerframework') || line.contains('org.jspecify')) {
annotatedForFiles.add(fvd.file.absolutePath)
return true;
}
}
}
}
String absolutejdkHome = file(annotatedJdkHome).absolutePath
int jdkDirStringSize = absolutejdkHome.size()
copy {
from(annotatedJdkHome)
into(outputDir)
for (String filename : annotatedForFiles) {
include filename.substring(jdkDirStringSize)
}
}
}
}

sourcesJar.dependsOn(copyAndMinimizeAnnotatedJdkFiles)
Expand Down
Loading
Loading