Refactor detect-split-packages Gradle plugin

- Use conventional plugin naming, i.e. "detect-split-packages" instead
   of applying plugin based on fully-qualified class name

 - Rename "diagnose" => "detect" consistently throughout plugin, task
   and method names and generally refactor naming throughout to follow
   "detect split packages" phrasing

 - Add Javadoc to DetectSplitPackagesPlugin

 - Improve error reporting when split packages are detected

   Upon detecting one or more split packages, `detectSplitPackages` now
   fails idiomatically, throwing a GradleException to signal task failure
   (as opposed to the previous approach of using an assert assertion), and
   the output reads as follows:

       $ gradle detectSplitPackages
       [...]
       :buildSrc:build UP-TO-DATE
       :detectSplitPackages FAILED

       FAILURE: Build failed with an exception.

       * What went wrong:
       Execution failed for task ':detectSplitPackages'.
       > The following split package(s) have been detected:
          - org.springframework.beans (split across spring-beans and spring-orm)
          - org.springframework.core.env (split across spring-context and spring-core)

 - DetectSplitPackagesTask now automatically attaches itself to `check`
   task lifecycle if the enclosing project contains a `check` task

 - DetectSplitPackagesTask adds itself to the 'Verification' task group,
   ensuring that it shows up correctly in `gradle tasks` task listings

 - packagesToScan now defaults to all subprojects; users may then
   customize this by removing individual subprojects from the collection

Issue: SPR-9990
This commit is contained in:
Chris Beams 2013-01-11 15:54:11 +01:00
parent b8f408ed5f
commit 654c07db34
4 changed files with 161 additions and 136 deletions

View File

@ -752,16 +752,16 @@ configure(rootProject) {
apply plugin: "docbook-reference"
apply plugin: "groovy"
apply plugin: "detect-split-packages"
apply from: "${gradleScriptDir}/jdiff.gradle"
apply plugin: org.springframework.build.gradle.SplitPackageDetectorPlugin
reference {
sourceDir = file("src/reference/docbook")
pdfFilename = "spring-framework-reference.pdf"
}
diagnoseSplitPackages {
projectsToScan = project.subprojects - project(":spring-instrument-tomcat") // SPR-10150
detectSplitPackages {
projectsToScan -= project(":spring-instrument-tomcat")
}
// don"t publish the default jar for the root project
@ -788,8 +788,6 @@ configure(rootProject) {
testCompile("hsqldb:hsqldb:${hsqldbVersion}")
}
check.dependsOn diagnoseSplitPackages
task api(type: Javadoc) {
group = "Documentation"
description = "Generates aggregated Javadoc API documentation."

View File

@ -0,0 +1,157 @@
/*
* Copyright 2002-2013 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.springframework.build.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
/**
* Gradle plugin that detects identically named, non-empty packages split across multiple
* subprojects, e.g. "org.springframework.context.annotation" existing in both spring-core
* and spring-aspects. Adds a 'detectSplitPackages' task to the current project's task
* collection. If the project already contains a 'check' task (i.e. is a typical Gradle
* project with the "java" plugin applied), the 'check' task will be updated to depend on
* the execution of 'detectSplitPackages'.
*
* By default, all subprojects will be scanned. Use the 'projectsToScan' task property to
* modify this value. Example usage:
*
* apply plugin: 'detect-split-packages // typically applied to root project
*
* detectSplitPackages {
* packagesToScan -= project(":spring-xyz") // scan every project but spring-xyz
* }
*
* @author Rob Winch
* @author Glyn Normington
* @author Chris Beams
*/
public class DetectSplitPackagesPlugin implements Plugin<Project> {
public void apply(Project project) {
def tasks = project.tasks
Task detectSplitPackages = tasks.add('detectSplitPackages', DetectSplitPackagesTask.class)
if (tasks.asMap.containsKey('check')) {
tasks.getByName('check').dependsOn detectSplitPackages
}
}
}
public class DetectSplitPackagesTask extends DefaultTask {
private static final String JAVA_FILE_SUFFIX = ".java"
private static final String PACKAGE_SEPARATOR = "."
private static final String HIDDEN_DIRECTORY_PREFIX = "."
@Input
Set<Project> projectsToScan = project.subprojects
public DetectSplitPackagesTask() {
this.group = 'Verification'
this.description = 'Detects packages split across two or more subprojects.'
}
@TaskAction
public void detectSplitPackages() {
def splitPackages = doDetectSplitPackages()
if (!splitPackages.isEmpty()) {
def message = "The following split package(s) have been detected:\n"
splitPackages.each { pkg, mod ->
message += " - ${pkg} (split across ${mod[0].name} and ${mod[1].name})\n"
}
throw new GradleException(message)
}
}
private Map<String, List<Project>> doDetectSplitPackages() {
def splitPackages = [:]
def mergedProjects = findMergedProjects()
def packagesByProject = mapPackagesByProject()
def projects = packagesByProject.keySet().toArray()
def nProjects = projects.length
for (int i = 0; i < nProjects - 1; i++) {
for (int j = i + 1; j < nProjects - 1; j++) {
def prj_i = projects[i]
def prj_j = projects[j]
def pkgs_i = new HashSet(packagesByProject.get(prj_i))
def pkgs_j = packagesByProject.get(prj_j)
pkgs_i.retainAll(pkgs_j)
if (!pkgs_i.isEmpty()
&& mergedProjects.get(prj_i) != prj_j
&& mergedProjects.get(prj_j) != prj_i) {
pkgs_i.each { pkg ->
def readablePkg = pkg.substring(1).replaceAll(File.separator, PACKAGE_SEPARATOR)
splitPackages[readablePkg] = [prj_i, prj_j]
}
}
}
}
return splitPackages;
}
private Map<Project, Set<String>> mapPackagesByProject() {
def packagesByProject = [:]
this.projectsToScan.each { Project p ->
def packages = new HashSet<String>()
p.sourceSets.main.java.srcDirs.each { File dir ->
findPackages(packages, dir, "")
}
if (!packages.isEmpty()) {
packagesByProject.put(p, packages)
}
}
return packagesByProject;
}
private Map<Project, Project> findMergedProjects() {
def mergedProjects = [:]
this.projectsToScan.findAll { p ->
p.plugins.findPlugin(MergePlugin)
}.findAll { p ->
p.merge.into
}.each { p ->
mergedProjects.put(p, p.merge.into)
}
return mergedProjects
}
private static void findPackages(Set<String> packages, File dir, String packagePath) {
def scanDir = new File(dir, packagePath)
def File[] javaFiles = scanDir.listFiles({ file ->
!file.isDirectory() && file.name.endsWith(JAVA_FILE_SUFFIX)
} as FileFilter)
if (javaFiles != null && javaFiles.length != 0) {
packages.add(packagePath)
}
scanDir.listFiles({ File file ->
file.isDirectory() && !file.name.startsWith(HIDDEN_DIRECTORY_PREFIX)
} as FileFilter).each { File subDir ->
findPackages(packages, dir, packagePath + File.separator + subDir.name)
}
}
}

View File

@ -1,131 +0,0 @@
/*
* Copyright 2013 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.springframework.build.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.artifacts.maven.Conf2ScopeMapping
import org.gradle.api.plugins.MavenPlugin
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.plugins.ide.eclipse.EclipsePlugin
import org.gradle.plugins.ide.eclipse.model.EclipseClasspath
import org.gradle.plugins.ide.idea.IdeaPlugin
class SplitPackageDetectorPlugin implements Plugin<Project> {
public void apply(Project project) {
Task diagnoseSplitPackages = project.tasks.add('diagnoseSplitPackages', SplitPackageDetectorTask.class)
diagnoseSplitPackages.setDescription('Detects packages which will be split across JARs')
}
}
public class SplitPackageDetectorTask extends DefaultTask {
@Input
Set<Project> projectsToScan
@TaskAction
public final void diagnoseSplitPackages() {
def Map<Project, Project> mergeMap = [:]
def projects = projectsToScan.findAll { it.plugins.findPlugin(org.springframework.build.gradle.MergePlugin) }.findAll { it.merge.into }
projects.each { p ->
mergeMap.put(p, p.merge.into)
}
def splitFound = new org.springframework.build.gradle.SplitPackageDetector(projectsToScan, mergeMap, project.logger).diagnoseSplitPackages();
assert !splitFound // see error log messages for details of split packages
}
}
class SplitPackageDetector {
private static final String HIDDEN_DIRECTORY_PREFIX = "."
private static final String JAVA_FILE_SUFFIX = ".java"
private static final String SRC_MAIN_JAVA = "src" + File.separator + "main" + File.separator + "java"
private static final String PACKAGE_SEPARATOR = "."
private final Map<Project, Project> mergeMap
private final Map<Project, Set<String>> pkgMap = [:]
private final logger
SplitPackageDetector(projectsToScan, mergeMap, logger) {
this.mergeMap = mergeMap
this.logger = logger
projectsToScan.each { Project p ->
def dir = p.projectDir
def packages = getPackagesInDirectory(dir)
if (!packages.isEmpty()) {
pkgMap.put(p, packages)
}
}
}
private File[] dirList(String dir) {
dirList(new File(dir))
}
private File[] dirList(File dir) {
dir.listFiles({ file -> file.isDirectory() && !file.getName().startsWith(HIDDEN_DIRECTORY_PREFIX) } as FileFilter)
}
private Set<String> getPackagesInDirectory(File dir) {
def pkgs = new HashSet<String>()
addPackagesInDirectory(pkgs, new File(dir, SRC_MAIN_JAVA), "")
return pkgs;
}
boolean diagnoseSplitPackages() {
def splitFound = false;
def projs = pkgMap.keySet().toArray()
def numProjects = projs.length
for (int i = 0; i < numProjects - 1; i++) {
for (int j = i + 1; j < numProjects - 1; j++) {
def pi = projs[i]
def pkgi = new HashSet(pkgMap.get(pi))
def pj = projs[j]
def pkgj = pkgMap.get(pj)
pkgi.retainAll(pkgj)
if (!pkgi.isEmpty() && mergeMap.get(pi) != pj && mergeMap.get(pj) != pi) {
pkgi.each { pkg ->
def readablePkg = pkg.substring(1).replaceAll(File.separator, PACKAGE_SEPARATOR)
logger.error("Package '$readablePkg' is split between $pi and $pj")
}
splitFound = true
}
}
}
return splitFound
}
private void addPackagesInDirectory(HashSet<String> packages, File dir, String pkg) {
def scanDir = new File(dir, pkg)
def File[] javaFiles = scanDir.listFiles({ file -> !file.isDirectory() && file.getName().endsWith(JAVA_FILE_SUFFIX) } as FileFilter)
if (javaFiles != null && javaFiles.length != 0) {
packages.add(pkg)
}
dirList(scanDir).each { File subDir ->
addPackagesInDirectory(packages, dir, pkg + File.separator + subDir.getName())
}
}
}

View File

@ -0,0 +1 @@
implementation-class=org.springframework.build.gradle.DetectSplitPackagesPlugin