diff --git a/build-tools/build-infra-shadow/build.gradle b/build-tools/build-infra-shadow/build.gradle index 7d8043837f98..2d9811086671 100644 --- a/build-tools/build-infra-shadow/build.gradle +++ b/build-tools/build-infra-shadow/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation deps.randomizedtesting.runner implementation deps.gjf implementation deps.jgit + implementation deps.gson implementation plugin(deps.plugins.carrotsearch.buildopts) implementation plugin(deps.plugins.carrotsearch.dependencychecks) diff --git a/build-tools/build-infra/build.gradle b/build-tools/build-infra/build.gradle index 84ed5c64b378..6a5331352a69 100644 --- a/build-tools/build-infra/build.gradle +++ b/build-tools/build-infra/build.gradle @@ -68,6 +68,7 @@ dependencies { implementation deps.flexmark.ext.tables implementation deps.gjf implementation deps.jgit + implementation deps.gson implementation plugin(deps.plugins.carrotsearch.buildopts) implementation plugin(deps.plugins.carrotsearch.dependencychecks) diff --git a/build-tools/build-infra/src/main/groovy/lucene.validation.jar-checks.gradle b/build-tools/build-infra/src/main/groovy/lucene.validation.jar-checks.gradle index 1550fdaf26f1..6043fa301836 100644 --- a/build-tools/build-infra/src/main/groovy/lucene.validation.jar-checks.gradle +++ b/build-tools/build-infra/src/main/groovy/lucene.validation.jar-checks.gradle @@ -16,6 +16,9 @@ */ import org.apache.commons.codec.digest.DigestUtils +import org.apache.lucene.gradle.validation.JarInfo +import org.apache.lucene.gradle.validation.JarValidationPlugin +import org.apache.lucene.gradle.validation.JarValidationTask // This adds validation of project dependencies: // 1) license file @@ -40,25 +43,6 @@ boolean failOnError = true // relative to the current project so they're not the same). def licensesDir = rootProject.layout.projectDirectory.dir("lucene/licenses") - -// All known license types. If 'noticeOptional' is true then -// the notice file must accompany the license. -def licenseTypes = [ - "ASL" : [name: "Apache Software License 2.0"], - "BSD" : [name: "Berkeley Software Distribution"], - //BSD like just means someone has taken the BSD license and put in their name, copyright, or it's a very similar license. - "BSD_LIKE": [name: "BSD like license"], - "CDDL" : [name: "Common Development and Distribution License", noticeOptional: true], - "CPL" : [name: "Common Public License"], - "EPL" : [name: "Eclipse Public License Version 1.0", noticeOptional: true], - "MIT" : [name: "Massachusetts Institute of Tech. License", noticeOptional: true], - "MPL" : [name: "Mozilla Public License", noticeOptional: true /* NOT SURE on the required notice */], - "PD" : [name: "Public Domain", noticeOptional: true], - "PDDL" : [name: "Public Domain Dedication and License", noticeOptional: true], - "SUN" : [name: "Sun Open Source License", noticeOptional: true], - "COMPOUND": [name: "Compound license (details in NOTICE file)."], -] - allprojects { def licensesTask = tasks.register("licenses", { group = 'Dependency validation' @@ -71,9 +55,7 @@ allprojects { } subprojects { - // initialize empty, because no checks for benchmark-jmh module. - ext.jarInfos = [] - + apply plugin: JarValidationPlugin // Configure jarValidation configuration for all projects. Any dependency // declared on this configuration (or any configuration it extends from) will // be verified. @@ -81,8 +63,6 @@ subprojects { jarValidation } - // For Java projects, add all dependencies from the following configurations - // to jar validation plugins.withType(JavaPlugin).configureEach { configurations { jarValidation { @@ -94,75 +74,6 @@ subprojects { } } - // Collects dependency JAR information for a project and saves it in - // project.ext.jarInfos. Each dependency has a map of attributes - // which make it easier to process it later on (name, hash, origin module, - // see the code below for details). - def collectJarInfos = tasks.register("collectJarInfos", { - dependsOn configurations.jarValidation - - doFirst { - // When gradle resolves a configuration it applies exclude rules from inherited configurations - // globally (this seems like a bug to me). So we process each inherited configuration independently - // but make sure there are no other dependencies on jarValidation itself. - if (!configurations.jarValidation.dependencies.isEmpty()) { - throw new GradleException("jarValidation must only inherit from other configurations (can't have its own dependencies).") - } - - def excludeRules = configurations.jarValidation.excludeRules - - ArrayDeque queue = new ArrayDeque<>() - configurations.jarValidation.extendsFrom.each { conf -> - if (excludeRules) { - conf = conf.copyRecursive() - conf.canBeResolved = true - conf.canBeConsumed = true - conf.excludeRules = excludeRules - } - if (conf.canBeResolved) { - queue.addAll(conf.resolvedConfiguration.firstLevelModuleDependencies) - } - } - - def visited = new HashSet<>() - def seenDeps = new HashSet<>() - def infos = [] - - while (!queue.isEmpty()) { - def dep = queue.removeFirst() - seenDeps.add(dep) - - // Skip any artifacts from Lucene modules. - if (!dep.moduleGroup.startsWith("org.apache.lucene")) { - // Make sure we don't keep visiting the same children over and over again - dep.children.each { child -> - if (!seenDeps.contains(child)) { - queue.add(child) - } - } - def digestUtils = new DigestUtils(DigestUtils.getSha1Digest()) - dep.moduleArtifacts.each { resolvedArtifact -> - def file = resolvedArtifact.file - if (visited.add(file)) { - infos.add([ - name : resolvedArtifact.name, - jarName : file.toPath().getFileName().toString(), - path : file, - module : resolvedArtifact.moduleVersion, - checksum : provider { digestUtils.digestAsHex(file).trim() }, - // We keep track of the files referenced by this dependency (sha, license, notice, etc.) - // so that we can determine unused dangling files later on. - referencedFiles: [] - ]) - } - } - } - } - - project.ext.jarInfos = infos.sort {a, b -> "${a.module}".compareTo("${b.module}")} - } - }) - // Verifies that each JAR has a corresponding checksum and that it matches actual JAR available for this dependency. tasks.register("validateJarChecksums", { group = 'Dependency validation' @@ -172,14 +83,14 @@ subprojects { doLast { def errors = [] - project.ext.jarInfos.each { dep -> + project.jarInfos.each {dep -> def expectedChecksumFile = licensesDir.file("${dep.jarName}.sha1").asFile if (!expectedChecksumFile.exists()) { errors << "Dependency checksum missing ('${dep.module}'), expected it at: ${expectedChecksumFile}" } else { - dep.referencedFiles += expectedChecksumFile + dep.addReferencedFile(expectedChecksumFile) def expected = expectedChecksumFile.getText("UTF-8").trim() - def actual = dep.checksum.get() + def actual = dep.checksum if (expected.compareToIgnoreCase(actual) != 0) { errors << "Dependency checksum mismatch ('${dep.module}'), expected it to be: ${expected}, but was: ${actual}" } else { @@ -204,67 +115,9 @@ subprojects { // where 'jar-or-prefix' can be any '-'-delimited prefix of the dependency JAR's name. // So for 'commons-io' it can be 'commons-io-LICENSE-foo.txt' or // 'commons-LICENSE.txt' - tasks.register("validateJarLicenses", { - group = 'Dependency validation' - description = "Validate license and notice files of dependencies" + tasks.named("validateJarLicenses").configure { dependsOn "collectJarInfos" - - doLast { - def errors = [] - project.ext.jarInfos.each { dep -> - def baseName = dep.name - def found = [] - def candidates = [] - while (true) { - candidates += licensesDir.file("${baseName}-LICENSE-[type].txt").asFile - found += fileTree(dir: licensesDir, include: "${baseName}-LICENSE-*.txt").files - def prefix = baseName.replaceAll(/[\-][^-]+$/, "") - if (found || prefix == baseName) { - break - } - baseName = prefix - } - - if (found.size() == 0) { - errors << "License file missing ('${dep.module}'), expected it at: ${candidates.join(" or ")}," + - " where [type] can be any of ${licenseTypes.keySet()}." - } else if (found.size() > 1) { - errors << "Multiple license files matching for ('${dep.module}'): ${found.join(", ")}" - } else { - def licenseFile = found.get(0) - dep.referencedFiles += licenseFile - def m = (licenseFile.name =~ /LICENSE-(.+)\.txt$/) - if (!m) throw new GradleException("License file name doesn't contain license type?: ${licenseFile.name}") - - def licenseName = m[0][1] - def licenseType = licenseTypes[licenseName] - if (!licenseType) { - errors << "Unknown license type suffix for ('${dep.module}'): ${licenseFile} (must be one of ${licenseTypes.keySet()})" - } else { - logger.log(LogLevel.INFO, "Dependency license file OK ('${dep.module}'): " + licenseName) - - // Look for sibling NOTICE file. - def noticeFile = file(licenseFile.path.replaceAll(/\-LICENSE-.+/, "-NOTICE.txt")) - if (noticeFile.exists()) { - dep.referencedFiles += noticeFile - logger.log(LogLevel.INFO, "Dependency notice file OK ('${dep.module}'): " + noticeFile) - } else if (!licenseType.noticeOptional) { - errors << "Notice file missing for ('${dep.module}'), expected it at: ${noticeFile}" - } - } - } - } - - if (errors) { - def msg = "Certain license/ notice files are missing:\n - " + errors.join("\n - ") - if (failOnError) { - throw new GradleException(msg) - } else { - logger.log(LogLevel.WARN, "WARNING: ${msg}") - } - } - } - }) + } tasks.named("licenses").configure { dependsOn "validateJarChecksums", "validateJarLicenses" @@ -283,9 +136,7 @@ configure(project(":lucene")) { ] def validationTasks = subprojects.collectMany { - it.tasks.matching { - it.name == "licenses" - } + it.tasks.withType(JarValidationTask.class) } def jarInfoTasks = subprojects.collectMany { it.tasks.matching { @@ -312,9 +163,9 @@ configure(project(":lucene")) { }) def updated = [] - jarInfoTasks.collectMany { task -> task.project.ext.jarInfos }.each { dep -> + jarInfoTasks.collectMany {task -> task.project.jarInfos}.each {dep -> def expectedChecksumFile = file("${licensesDir}/${dep.jarName}.sha1") - def actual = dep.checksum.get() + def actual = dep.checksum if (expectedChecksumFile.exists()) { def expected = expectedChecksumFile.getText("UTF-8").trim() if (expected.compareToIgnoreCase(actual) == 0) { @@ -326,7 +177,7 @@ configure(project(":lucene")) { expectedChecksumFile.write(actual + "\n", "UTF-8") } - updated.sort().each { line -> logger.log(LogLevel.LIFECYCLE, line) } + updated.sort().each {line -> logger.log(LogLevel.LIFECYCLE, line)} } }) @@ -365,7 +216,7 @@ configure(project(":lucene")) { // Only collect for enabled tasks: https://issues.apache.org/jira/browse/LUCENE-9780 .findAll { it.enabled } .collectMany { task -> - task.project.ext.jarInfos.collectMany { it.referencedFiles } + task.jarInfos.collectMany { it.referencedFiles } } .collect { it.toString() } diff --git a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/GenerateJarInfosTask.java b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/GenerateJarInfosTask.java new file mode 100644 index 000000000000..67bca87dd630 --- /dev/null +++ b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/GenerateJarInfosTask.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.lucene.gradle.validation; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.codec.digest.DigestUtils; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.artifacts.ArtifactCollection; +import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.internal.component.external.model.ModuleComponentArtifactIdentifier; + +/** + * Collects dependency JAR information for a project and saves it in project.jarInfos and in an + * output file. Each dependency has a map of attributes which make it easier to process it later on + * (name, hash, module name) + */ +public abstract class GenerateJarInfosTask extends DefaultTask { + + /** + * We have to use ArtifactCollection instead of ResolvedArtifactResult here as we're running into + * an issue in Gradle: https://github.com/gradle/gradle/issues/27582 + */ + @Internal + abstract ListProperty getRuntimeArtifacts(); + + @InputFiles + abstract ConfigurableFileCollection getDependencies(); + + @OutputFile + abstract RegularFileProperty getOutputFile(); + + @TaskAction + void generateJarInfos() throws IOException { + var digestUtils = new DigestUtils(DigestUtils.getSha1Digest()); + Set jarInfos = new LinkedHashSet<>(); + ArrayDeque queue = new ArrayDeque<>(); + + getRuntimeArtifacts() + .get() + .forEach( + r -> { + queue.addAll(r.getArtifacts()); + }); + + Set seenIds = new HashSet<>(); + Set visited = new HashSet<>(); + while (queue.size() > 0) { + ResolvedArtifactResult result = queue.removeFirst(); + ComponentArtifactIdentifier id = result.getId(); + seenIds.add(id); + if (id instanceof ModuleComponentArtifactIdentifier + && !((ModuleComponentArtifactIdentifier) id) + .getComponentIdentifier() + .getModule() + .startsWith("org.apache.lucene")) { + ModuleComponentArtifactIdentifier id2 = (ModuleComponentArtifactIdentifier) id; + File file = result.getFile(); + if (visited.add(file)) { + jarInfos.add( + new JarInfo( + id2.getComponentIdentifier().getModule(), + file.toPath().getFileName().toString(), + id.getComponentIdentifier().getDisplayName(), + digestUtils.digestAsHex(file).trim())); + } + } + } + + List sorted = + jarInfos.stream() + .sorted(Comparator.comparing(JarInfo::getName)) + .collect(Collectors.toList()); + writeToFile(sorted); + configureExtension(sorted); + } + + @SuppressWarnings("all") + private void configureExtension(List sorted) { + // TODO: remove ones we ported depentend tasks over to rely on report file. + ((List) getProject().getExtensions().getByName("jarInfos")).addAll(sorted); + } + + private void writeToFile(Collection jarInfos) throws IOException { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + try (FileWriter writer = new FileWriter(getOutputFile().getAsFile().get())) { + gson.toJson(jarInfos, writer); + } catch (IOException e) { + new GradleException("Unable to write to file", e); + } + } +} diff --git a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarInfo.java b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarInfo.java new file mode 100644 index 000000000000..d47816363e36 --- /dev/null +++ b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarInfo.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.lucene.gradle.validation; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public final class JarInfo { + + private final String name; + private final String jarName; + private final String module; + private final String checksum; + + // We keep track of the files referenced by this dependency (sha, license, notice, etc.) + // so that we can determine unused dangling files later on. + private final transient List referencedFiles; + + public JarInfo(String name, String jarName, String module, String checksum) { + this.name = name; + this.jarName = jarName; + this.module = module; + this.checksum = checksum; + this.referencedFiles = new ArrayList<>(); + } + + public void addReferencedFile(File file) { + referencedFiles.add(file); + } + + public String getName() { + return name; + } + + public String getJarName() { + return jarName; + } + + public String getModule() { + return module; + } + + public String getChecksum() { + return checksum; + } + + public List getReferencedFiles() { + return referencedFiles; + } +} diff --git a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarLicenseValidationTask.java b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarLicenseValidationTask.java new file mode 100644 index 000000000000..cada7d9c1981 --- /dev/null +++ b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarLicenseValidationTask.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.lucene.gradle.validation; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.inject.Inject; +import org.gradle.api.GradleException; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.internal.file.FileOperations; +import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.TaskAction; + +public abstract class JarLicenseValidationTask extends JarValidationTask { + + // All known license types. If 'noticeOptional' is true then + // the notice file must accompany the license. + public static final Map LICENSE_TYPES; + + static { + Map tmp = new LinkedHashMap<>(); + tmp.put("ASL", new LicenseType("Apache Software License 2.0")); + tmp.put("BSD", new LicenseType("Berkeley Software Distribution")); + // BSD-like: someone tweaked the BSD license to add their own copyright, etc. + tmp.put("BSD_LIKE", new LicenseType("BSD like license")); + tmp.put( + "CDDL", + new LicenseType("Common Development and Distribution License", /*noticeOptional*/ true)); + tmp.put("CPL", new LicenseType("Common Public License")); + tmp.put("EPL", new LicenseType("Eclipse Public License 1.0", true)); + tmp.put("MIT", new LicenseType("Massachusetts Institute of Tech. License", true)); + tmp.put("MPL", new LicenseType("Mozilla Public License", true /* not sure */)); + tmp.put("PD", new LicenseType("Public Domain", true)); + tmp.put("PDDL", new LicenseType("Public Domain Dedication and License", true)); + tmp.put("SUN", new LicenseType("Sun Open Source License", true)); + tmp.put("COMPOUND", new LicenseType("Compound license (details in NOTICE file).")); + + LICENSE_TYPES = Map.copyOf(tmp); // make it unmodifiable + } + + @InputDirectory + public abstract RegularFileProperty getLicenseDir(); + + @Inject + public JarLicenseValidationTask() { + setGroup("Dependency validation"); + setDescription("Validate license and notice files of dependencies"); + } + + @TaskAction + void validateJar() { + List errors = new ArrayList<>(); + File licensesDir = getLicenseDir().get().getAsFile(); + + jarInfos.forEach( + dep -> { + var baseName = dep.getName(); + List found = new ArrayList<>(); + List candidates = new ArrayList<>(); + while (true) { + candidates.add(new File(licensesDir, baseName + "-LICENSE-[type].txt")); + found.addAll( + getFileOperations() + .fileTree(Map.of("dir", licensesDir, "include", baseName + "-LICENSE-*.txt")) + .getFiles()); + String prefix = baseName.replaceAll("[\\\\-][^-]+\\$", ""); + if (found.size() > 0 || prefix == baseName) { + break; + } + baseName = prefix; + } + + if (found.size() == 0) { + errors.add( + "License file missing ('" + + dep.getModule() + + "'), expected it at: " + + candidates.stream().map(File::getPath).collect(Collectors.joining(" or ")) + + " where [type] can be any of ${licenseTypes.keySet()}."); + } else if (found.size() > 1) { + errors.add( + "Multiple license files matching for ('" + + dep.getModule() + + "')): " + + found.stream().map(File::getPath).collect(Collectors.joining(", "))); + } else { + File licenseFile = found.get(0); + dep.addReferencedFile(licenseFile); + Pattern pattern = Pattern.compile("LICENSE-(.+)\\.txt$"); + Matcher matcher = pattern.matcher(licenseFile.getName()); + if (!matcher.find()) { + throw new GradleException( + "License file name doesn't contain license type?: " + licenseFile.getName()); + } + + String licenseName = matcher.group(1); + LicenseType licenseType = LICENSE_TYPES.get(licenseName); + + if (licenseType == null) { + errors.add( + "Unknown license type suffix for ('" + + dep.getModule() + + "'): " + + licenseFile.getPath() + + " (must be one of " + + LICENSE_TYPES.keySet() + + ")"); + } else { + getLogger() + .info( + String.format( + "Dependency license file OK ('%s'): " + licenseName, dep.getModule())); + + // Look for sibling NOTICE file. + File noticeFile = + new File( + licenseFile + .getPath() // same directory as the LICENSE file + .replaceAll("-LICENSE-.+", "-NOTICE.txt")); // LICENSE-… → NOTICE.txt + + if (noticeFile.exists()) { + dep.addReferencedFile(noticeFile); + getLogger() + .info("Dependency notice file OK ('%s'): " + noticeFile, dep.getModule()); + } else if (!licenseType.isNoticeOptional()) { + errors.add( + String.format( + "Notice file missing for ('%s'), expected it at: %s", + dep.getModule(), noticeFile)); + } + } + } + }); + if (errors.size() > 0) { + String msg = + "Certain license/ notice files are missing:\n - " + + errors.stream().collect(Collectors.joining("\n - ")); + // TODO: make this a verification task to support optional failOnError + throw new GradleException(msg); + } + } + + @Inject + protected abstract FileOperations getFileOperations(); +} diff --git a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarValidationPlugin.java b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarValidationPlugin.java new file mode 100644 index 000000000000..9afb5944f4c3 --- /dev/null +++ b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarValidationPlugin.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.lucene.gradle.validation; + +import java.util.ArrayList; +import java.util.List; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; + +public abstract class JarValidationPlugin implements Plugin { + + @Override + public void apply(Project project) { + List jarInfos = new ArrayList<>(); + project.getExtensions().add("jarInfos", jarInfos); + + project + .getTasks() + .register( + "collectJarInfos", + GenerateJarInfosTask.class, + task -> { + // For Java projects, add all dependencies from each source set + project + .getPlugins() + .withType( + JavaPlugin.class, + _ -> { + var java = project.getExtensions().getByType(JavaPluginExtension.class); + java.getSourceSets() + .configureEach( + sourceSet -> { + var compileClassPathConfiguration = + project + .getConfigurations() + .getByName( + sourceSet.getCompileClasspathConfigurationName()); + var runtimeClassPathConfiguration = + project + .getConfigurations() + .getByName( + sourceSet.getRuntimeClasspathConfigurationName()); + task.getDependencies().from(compileClassPathConfiguration); + task.getDependencies().from(runtimeClassPathConfiguration); + + task.getRuntimeArtifacts() + .add( + compileClassPathConfiguration + .getIncoming() + .getArtifacts()); + task.getRuntimeArtifacts() + .add( + runtimeClassPathConfiguration + .getIncoming() + .getArtifacts()); + }); + }); + // TODO remove ones downstream dependencies have been ported to file dependencies + task.getOutputs().upToDateWhen(_ -> false); + task.getOutputFile() + .set(project.getLayout().getBuildDirectory().file("reports/jarInfos.json")); + }); + + project + .getTasks() + .withType(JarValidationTask.class) + .configureEach(task -> task.setJarInfos(jarInfos)); + project + .getTasks() + .register( + "validateJarLicenses", + JarLicenseValidationTask.class, + task -> + task.getLicenseDir() + .fileValue( + project + .getLayout() + .getSettingsDirectory() + .dir("lucene/licenses") + .getAsFile())); + } +} diff --git a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarValidationTask.java b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarValidationTask.java new file mode 100644 index 000000000000..65e9a2cc6cbf --- /dev/null +++ b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/JarValidationTask.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.lucene.gradle.validation; + +import java.util.ArrayList; +import java.util.List; +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.Internal; + +public abstract class JarValidationTask extends DefaultTask { + + List jarInfos = new ArrayList<>(); + + @Internal + public List getJarInfos() { + return jarInfos; + } + + public void setJarInfos(List jarInfos) { + this.jarInfos = jarInfos; + } +} diff --git a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/LicenseType.java b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/LicenseType.java new file mode 100644 index 000000000000..2e87943c6643 --- /dev/null +++ b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/validation/LicenseType.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.lucene.gradle.validation; + +/** Immutable value object for one license type. */ +public class LicenseType { + + private final String name; + private final boolean noticeOptional; + + public LicenseType(String name, boolean noticeOptional) { + this.name = name; + this.noticeOptional = noticeOptional; + } + + public LicenseType(String name) { + this(name, false); + } + + public String getName() { + return name; + } + + public boolean isNoticeOptional() { + return noticeOptional; + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd2aa7aad289..575910d80050 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,8 @@ flexmark = "0.64.8" googleJavaFormat = "1.27.0" # gradle build support groovy = "4.0.27" +# serializing build analytics +gson = "2.10.1" # test assertions hamcrest = "3.0" # analysis/icu/, gradle regeneration unicode support @@ -76,6 +78,7 @@ flexmark-ext-autolink = { module = "com.vladsch.flexmark:flexmark-ext-autolink", flexmark-ext-tables = { module = "com.vladsch.flexmark:flexmark-ext-tables", version.ref = "flexmark" } gjf = { module = "com.google.googlejavaformat:google-java-format", version.ref = "googleJavaFormat" } groovy = { module = "org.apache.groovy:groovy-all", version.ref = "groovy" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" } javacc = { module = "net.java.dev.javacc:javacc", version.ref = "javacc" }