|
|
|
|
@ -58,34 +58,30 @@ import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
@@ -58,34 +58,30 @@ import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
|
|
|
|
|
*/ |
|
|
|
|
public abstract class CheckBom extends DefaultTask { |
|
|
|
|
|
|
|
|
|
private final Provider<ResolvedBom> resolvedBom; |
|
|
|
|
|
|
|
|
|
private final ConfigurationContainer configurations; |
|
|
|
|
|
|
|
|
|
private final DependencyHandler dependencies; |
|
|
|
|
|
|
|
|
|
private final BomExtension bom; |
|
|
|
|
|
|
|
|
|
private final BomResolver bomResolver; |
|
|
|
|
private final List<LibraryCheck> checks; |
|
|
|
|
|
|
|
|
|
@Inject |
|
|
|
|
public CheckBom(BomExtension bom) { |
|
|
|
|
this.configurations = getProject().getConfigurations(); |
|
|
|
|
this.dependencies = getProject().getDependencies(); |
|
|
|
|
ConfigurationContainer configurations = getProject().getConfigurations(); |
|
|
|
|
DependencyHandler dependencies = getProject().getDependencies(); |
|
|
|
|
Provider<ResolvedBom> resolvedBom = getResolvedBomFile().map(RegularFile::getAsFile).map(ResolvedBom::readFrom); |
|
|
|
|
this.checks = List.of(new CheckExclusions(configurations, dependencies), new CheckProhibitedVersions(), |
|
|
|
|
new CheckVersionAlignment(), |
|
|
|
|
new CheckDependencyManagementAlignment(resolvedBom, configurations, dependencies)); |
|
|
|
|
this.bom = bom; |
|
|
|
|
this.resolvedBom = getResolvedBomFile().map(RegularFile::getAsFile).map(ResolvedBom::readFrom); |
|
|
|
|
this.bomResolver = new BomResolver(this.configurations, this.dependencies); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@InputFile |
|
|
|
|
@PathSensitive(PathSensitivity.RELATIVE) |
|
|
|
|
abstract RegularFileProperty getResolvedBomFile(); |
|
|
|
|
public abstract RegularFileProperty getResolvedBomFile(); |
|
|
|
|
|
|
|
|
|
@TaskAction |
|
|
|
|
void checkBom() { |
|
|
|
|
List<String> errors = new ArrayList<>(); |
|
|
|
|
for (Library library : this.bom.getLibraries()) { |
|
|
|
|
checkLibrary(library, errors); |
|
|
|
|
errors.addAll(checkLibrary(library)); |
|
|
|
|
} |
|
|
|
|
if (!errors.isEmpty()) { |
|
|
|
|
System.out.println(); |
|
|
|
|
@ -95,165 +91,229 @@ public abstract class CheckBom extends DefaultTask {
@@ -95,165 +91,229 @@ public abstract class CheckBom extends DefaultTask {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void checkLibrary(Library library, List<String> errors) { |
|
|
|
|
private List<String> checkLibrary(Library library) { |
|
|
|
|
List<String> libraryErrors = new ArrayList<>(); |
|
|
|
|
checkExclusions(library, libraryErrors); |
|
|
|
|
checkProhibitedVersions(library, libraryErrors); |
|
|
|
|
checkVersionAlignment(library, libraryErrors); |
|
|
|
|
checkDependencyManagementAlignment(library, libraryErrors); |
|
|
|
|
this.checks.stream().flatMap((check) -> check.check(library).stream()).forEach(libraryErrors::add); |
|
|
|
|
List<String> errors = new ArrayList<>(); |
|
|
|
|
if (!libraryErrors.isEmpty()) { |
|
|
|
|
errors.add(library.getName()); |
|
|
|
|
for (String libraryError : libraryErrors) { |
|
|
|
|
errors.add(" - " + libraryError); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return errors; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private interface LibraryCheck { |
|
|
|
|
|
|
|
|
|
List<String> check(Library library); |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void checkExclusions(Library library, List<String> errors) { |
|
|
|
|
for (Group group : library.getGroups()) { |
|
|
|
|
for (Module module : group.getModules()) { |
|
|
|
|
if (!module.getExclusions().isEmpty()) { |
|
|
|
|
checkExclusions(group.getId(), module, library.getVersion().getVersion(), errors); |
|
|
|
|
private static final class CheckExclusions implements LibraryCheck { |
|
|
|
|
|
|
|
|
|
private final ConfigurationContainer configurations; |
|
|
|
|
|
|
|
|
|
private final DependencyHandler dependencies; |
|
|
|
|
|
|
|
|
|
private CheckExclusions(ConfigurationContainer configurations, DependencyHandler dependencies) { |
|
|
|
|
this.configurations = configurations; |
|
|
|
|
this.dependencies = dependencies; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public List<String> check(Library library) { |
|
|
|
|
List<String> errors = new ArrayList<>(); |
|
|
|
|
for (Group group : library.getGroups()) { |
|
|
|
|
for (Module module : group.getModules()) { |
|
|
|
|
if (!module.getExclusions().isEmpty()) { |
|
|
|
|
checkExclusions(group.getId(), module, library.getVersion().getVersion(), errors); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return errors; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void checkExclusions(String groupId, Module module, DependencyVersion version, List<String> errors) { |
|
|
|
|
Set<String> resolved = this.configurations |
|
|
|
|
.detachedConfiguration(this.dependencies.create(groupId + ":" + module.getName() + ":" + version)) |
|
|
|
|
.getResolvedConfiguration() |
|
|
|
|
.getResolvedArtifacts() |
|
|
|
|
.stream() |
|
|
|
|
.map((artifact) -> artifact.getModuleVersion().getId()) |
|
|
|
|
.map((id) -> id.getGroup() + ":" + id.getModule().getName()) |
|
|
|
|
.collect(Collectors.toSet()); |
|
|
|
|
Set<String> exclusions = module.getExclusions() |
|
|
|
|
.stream() |
|
|
|
|
.map((exclusion) -> exclusion.getGroupId() + ":" + exclusion.getArtifactId()) |
|
|
|
|
.collect(Collectors.toSet()); |
|
|
|
|
Set<String> unused = new TreeSet<>(); |
|
|
|
|
for (String exclusion : exclusions) { |
|
|
|
|
if (!resolved.contains(exclusion)) { |
|
|
|
|
if (exclusion.endsWith(":*")) { |
|
|
|
|
String group = exclusion.substring(0, exclusion.indexOf(':') + 1); |
|
|
|
|
if (resolved.stream().noneMatch((candidate) -> candidate.startsWith(group))) { |
|
|
|
|
private void checkExclusions(String groupId, Module module, DependencyVersion version, List<String> errors) { |
|
|
|
|
Set<String> resolved = this.configurations |
|
|
|
|
.detachedConfiguration(this.dependencies.create(groupId + ":" + module.getName() + ":" + version)) |
|
|
|
|
.getResolvedConfiguration() |
|
|
|
|
.getResolvedArtifacts() |
|
|
|
|
.stream() |
|
|
|
|
.map((artifact) -> artifact.getModuleVersion().getId()) |
|
|
|
|
.map((id) -> id.getGroup() + ":" + id.getModule().getName()) |
|
|
|
|
.collect(Collectors.toSet()); |
|
|
|
|
Set<String> exclusions = module.getExclusions() |
|
|
|
|
.stream() |
|
|
|
|
.map((exclusion) -> exclusion.getGroupId() + ":" + exclusion.getArtifactId()) |
|
|
|
|
.collect(Collectors.toSet()); |
|
|
|
|
Set<String> unused = new TreeSet<>(); |
|
|
|
|
for (String exclusion : exclusions) { |
|
|
|
|
if (!resolved.contains(exclusion)) { |
|
|
|
|
if (exclusion.endsWith(":*")) { |
|
|
|
|
String group = exclusion.substring(0, exclusion.indexOf(':') + 1); |
|
|
|
|
if (resolved.stream().noneMatch((candidate) -> candidate.startsWith(group))) { |
|
|
|
|
unused.add(exclusion); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
else { |
|
|
|
|
unused.add(exclusion); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
else { |
|
|
|
|
unused.add(exclusion); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
exclusions.removeAll(resolved); |
|
|
|
|
if (!unused.isEmpty()) { |
|
|
|
|
errors.add("Unnecessary exclusions on " + groupId + ":" + module.getName() + ": " + exclusions); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
exclusions.removeAll(resolved); |
|
|
|
|
if (!unused.isEmpty()) { |
|
|
|
|
errors.add("Unnecessary exclusions on " + groupId + ":" + module.getName() + ": " + exclusions); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void checkProhibitedVersions(Library library, List<String> errors) { |
|
|
|
|
ArtifactVersion currentVersion = new DefaultArtifactVersion(library.getVersion().getVersion().toString()); |
|
|
|
|
for (ProhibitedVersion prohibited : library.getProhibitedVersions()) { |
|
|
|
|
if (prohibited.isProhibited(library.getVersion().getVersion().toString())) { |
|
|
|
|
errors.add("Current version " + currentVersion + " is prohibited"); |
|
|
|
|
} |
|
|
|
|
else { |
|
|
|
|
VersionRange versionRange = prohibited.getRange(); |
|
|
|
|
if (versionRange != null) { |
|
|
|
|
for (Restriction restriction : versionRange.getRestrictions()) { |
|
|
|
|
ArtifactVersion upperBound = restriction.getUpperBound(); |
|
|
|
|
if (upperBound == null) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
int comparison = currentVersion.compareTo(upperBound); |
|
|
|
|
if ((restriction.isUpperBoundInclusive() && comparison <= 0) |
|
|
|
|
|| ((!restriction.isUpperBoundInclusive()) && comparison < 0)) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
private static final class CheckProhibitedVersions implements LibraryCheck { |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public List<String> check(Library library) { |
|
|
|
|
List<String> errors = new ArrayList<>(); |
|
|
|
|
ArtifactVersion currentVersion = new DefaultArtifactVersion(library.getVersion().getVersion().toString()); |
|
|
|
|
for (ProhibitedVersion prohibited : library.getProhibitedVersions()) { |
|
|
|
|
if (prohibited.isProhibited(library.getVersion().getVersion().toString())) { |
|
|
|
|
errors.add("Current version " + currentVersion + " is prohibited"); |
|
|
|
|
} |
|
|
|
|
else { |
|
|
|
|
VersionRange versionRange = prohibited.getRange(); |
|
|
|
|
if (versionRange != null) { |
|
|
|
|
check(currentVersion, versionRange, errors); |
|
|
|
|
} |
|
|
|
|
errors.add("Version range " + versionRange + " is ineffective as the current version, " |
|
|
|
|
+ currentVersion + ", is greater than its upper bound"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return errors; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void checkVersionAlignment(Library library, List<String> errors) { |
|
|
|
|
VersionAlignment versionAlignment = library.getVersionAlignment(); |
|
|
|
|
if (versionAlignment == null) { |
|
|
|
|
return; |
|
|
|
|
private void check(ArtifactVersion currentVersion, VersionRange versionRange, List<String> errors) { |
|
|
|
|
for (Restriction restriction : versionRange.getRestrictions()) { |
|
|
|
|
ArtifactVersion upperBound = restriction.getUpperBound(); |
|
|
|
|
if (upperBound == null) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
int comparison = currentVersion.compareTo(upperBound); |
|
|
|
|
if ((restriction.isUpperBoundInclusive() && comparison <= 0) |
|
|
|
|
|| ((!restriction.isUpperBoundInclusive()) && comparison < 0)) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
errors.add("Version range " + versionRange + " is ineffective as the current version, " + currentVersion |
|
|
|
|
+ ", is greater than its upper bound"); |
|
|
|
|
} |
|
|
|
|
Set<String> alignedVersions = versionAlignment.resolve(); |
|
|
|
|
if (alignedVersions.size() == 1) { |
|
|
|
|
String alignedVersion = alignedVersions.iterator().next(); |
|
|
|
|
if (!alignedVersion.equals(library.getVersion().getVersion().toString())) { |
|
|
|
|
errors.add("Version " + library.getVersion().getVersion() + " is misaligned. It should be " |
|
|
|
|
+ alignedVersion + "."); |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static final class CheckVersionAlignment implements LibraryCheck { |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public List<String> check(Library library) { |
|
|
|
|
List<String> errors = new ArrayList<>(); |
|
|
|
|
VersionAlignment versionAlignment = library.getVersionAlignment(); |
|
|
|
|
if (versionAlignment != null) { |
|
|
|
|
check(versionAlignment, library, errors); |
|
|
|
|
} |
|
|
|
|
return errors; |
|
|
|
|
} |
|
|
|
|
else { |
|
|
|
|
if (alignedVersions.isEmpty()) { |
|
|
|
|
errors.add("Version alignment requires a single version but none were found."); |
|
|
|
|
|
|
|
|
|
private void check(VersionAlignment versionAlignment, Library library, List<String> errors) { |
|
|
|
|
Set<String> alignedVersions = versionAlignment.resolve(); |
|
|
|
|
if (alignedVersions.size() == 1) { |
|
|
|
|
String alignedVersion = alignedVersions.iterator().next(); |
|
|
|
|
if (!alignedVersion.equals(library.getVersion().getVersion().toString())) { |
|
|
|
|
errors.add("Version " + library.getVersion().getVersion() + " is misaligned. It should be " |
|
|
|
|
+ alignedVersion + "."); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
else { |
|
|
|
|
errors.add("Version alignment requires a single version but " + alignedVersions.size() + " were found: " |
|
|
|
|
+ alignedVersions + "."); |
|
|
|
|
if (alignedVersions.isEmpty()) { |
|
|
|
|
errors.add("Version alignment requires a single version but none were found."); |
|
|
|
|
} |
|
|
|
|
else { |
|
|
|
|
errors.add("Version alignment requires a single version but " + alignedVersions.size() |
|
|
|
|
+ " were found: " + alignedVersions + "."); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void checkDependencyManagementAlignment(Library library, List<String> errors) { |
|
|
|
|
String alignsWithBom = library.getAlignsWithBom(); |
|
|
|
|
if (alignsWithBom == null) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
Bom mavenBom = this.bomResolver.resolveMavenBom(alignsWithBom + ":" + library.getVersion().getVersion()); |
|
|
|
|
ResolvedBom resolvedBom = this.resolvedBom.get(); |
|
|
|
|
Optional<ResolvedLibrary> resolvedLibrary = resolvedBom.libraries() |
|
|
|
|
.stream() |
|
|
|
|
.filter((candidate) -> candidate.name().equals(library.getName())) |
|
|
|
|
.findFirst(); |
|
|
|
|
if (!resolvedLibrary.isPresent()) { |
|
|
|
|
throw new RuntimeException("Library '%s' not found in resolved bom".formatted(library.getName())); |
|
|
|
|
} |
|
|
|
|
checkDependencyManagementAlignment(resolvedLibrary.get(), mavenBom, errors); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void checkDependencyManagementAlignment(ResolvedLibrary library, Bom mavenBom, List<String> errors) { |
|
|
|
|
List<Id> managedByLibrary = library.managedDependencies(); |
|
|
|
|
List<Id> managedByBom = managedDependenciesOf(mavenBom); |
|
|
|
|
private static final class CheckDependencyManagementAlignment implements LibraryCheck { |
|
|
|
|
|
|
|
|
|
List<Id> missing = new ArrayList<>(managedByBom); |
|
|
|
|
missing.removeAll(managedByLibrary); |
|
|
|
|
private final Provider<ResolvedBom> resolvedBom; |
|
|
|
|
|
|
|
|
|
List<Id> unexpected = new ArrayList<>(managedByLibrary); |
|
|
|
|
unexpected.removeAll(managedByBom); |
|
|
|
|
if (missing.isEmpty() && unexpected.isEmpty()) { |
|
|
|
|
return; |
|
|
|
|
private final BomResolver bomResolver; |
|
|
|
|
|
|
|
|
|
private CheckDependencyManagementAlignment(Provider<ResolvedBom> resolvedBom, |
|
|
|
|
ConfigurationContainer configurations, DependencyHandler dependencies) { |
|
|
|
|
this.resolvedBom = resolvedBom; |
|
|
|
|
this.bomResolver = new BomResolver(configurations, dependencies); |
|
|
|
|
} |
|
|
|
|
String error = "Dependency management does not align with " + mavenBom.id() + ":"; |
|
|
|
|
if (!missing.isEmpty()) { |
|
|
|
|
error = error + "%n - Missing:%n %s".formatted(String.join("\n ", |
|
|
|
|
missing.stream().map((dependency) -> dependency.toString()).toList())); |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public List<String> check(Library library) { |
|
|
|
|
List<String> errors = new ArrayList<>(); |
|
|
|
|
String alignsWithBom = library.getAlignsWithBom(); |
|
|
|
|
if (alignsWithBom != null) { |
|
|
|
|
Bom mavenBom = this.bomResolver |
|
|
|
|
.resolveMavenBom(alignsWithBom + ":" + library.getVersion().getVersion()); |
|
|
|
|
ResolvedLibrary resolvedLibrary = getResolvedLibrary(library); |
|
|
|
|
checkDependencyManagementAlignment(resolvedLibrary, mavenBom, errors); |
|
|
|
|
} |
|
|
|
|
return errors; |
|
|
|
|
} |
|
|
|
|
if (!unexpected.isEmpty()) { |
|
|
|
|
error = error + "%n - Unexpected:%n %s".formatted(String.join("\n ", |
|
|
|
|
unexpected.stream().map((dependency) -> dependency.toString()).toList())); |
|
|
|
|
|
|
|
|
|
private ResolvedLibrary getResolvedLibrary(Library library) { |
|
|
|
|
ResolvedBom resolvedBom = this.resolvedBom.get(); |
|
|
|
|
Optional<ResolvedLibrary> resolvedLibrary = resolvedBom.libraries() |
|
|
|
|
.stream() |
|
|
|
|
.filter((candidate) -> candidate.name().equals(library.getName())) |
|
|
|
|
.findFirst(); |
|
|
|
|
if (!resolvedLibrary.isPresent()) { |
|
|
|
|
throw new RuntimeException("Library '%s' not found in resolved bom".formatted(library.getName())); |
|
|
|
|
} |
|
|
|
|
return resolvedLibrary.get(); |
|
|
|
|
} |
|
|
|
|
errors.add(error); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private List<Id> managedDependenciesOf(Bom mavenBom) { |
|
|
|
|
List<Id> managedDependencies = new ArrayList<>(); |
|
|
|
|
managedDependencies.addAll(mavenBom.managedDependencies()); |
|
|
|
|
if (mavenBom.parent() != null) { |
|
|
|
|
managedDependencies.addAll(managedDependenciesOf(mavenBom.parent())); |
|
|
|
|
private void checkDependencyManagementAlignment(ResolvedLibrary library, Bom mavenBom, List<String> errors) { |
|
|
|
|
List<Id> managedByLibrary = library.managedDependencies(); |
|
|
|
|
List<Id> managedByBom = managedDependenciesOf(mavenBom); |
|
|
|
|
|
|
|
|
|
List<Id> missing = new ArrayList<>(managedByBom); |
|
|
|
|
missing.removeAll(managedByLibrary); |
|
|
|
|
|
|
|
|
|
List<Id> unexpected = new ArrayList<>(managedByLibrary); |
|
|
|
|
unexpected.removeAll(managedByBom); |
|
|
|
|
if (missing.isEmpty() && unexpected.isEmpty()) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
String error = "Dependency management does not align with " + mavenBom.id() + ":"; |
|
|
|
|
if (!missing.isEmpty()) { |
|
|
|
|
error = error + "%n - Missing:%n %s".formatted(String.join("\n ", |
|
|
|
|
missing.stream().map((dependency) -> dependency.toString()).toList())); |
|
|
|
|
} |
|
|
|
|
if (!unexpected.isEmpty()) { |
|
|
|
|
error = error + "%n - Unexpected:%n %s".formatted(String.join("\n ", |
|
|
|
|
unexpected.stream().map((dependency) -> dependency.toString()).toList())); |
|
|
|
|
} |
|
|
|
|
errors.add(error); |
|
|
|
|
} |
|
|
|
|
for (Bom importedBom : mavenBom.importedBoms()) { |
|
|
|
|
managedDependencies.addAll(managedDependenciesOf(importedBom)); |
|
|
|
|
|
|
|
|
|
private List<Id> managedDependenciesOf(Bom mavenBom) { |
|
|
|
|
List<Id> managedDependencies = new ArrayList<>(); |
|
|
|
|
managedDependencies.addAll(mavenBom.managedDependencies()); |
|
|
|
|
if (mavenBom.parent() != null) { |
|
|
|
|
managedDependencies.addAll(managedDependenciesOf(mavenBom.parent())); |
|
|
|
|
} |
|
|
|
|
for (Bom importedBom : mavenBom.importedBoms()) { |
|
|
|
|
managedDependencies.addAll(managedDependenciesOf(importedBom)); |
|
|
|
|
} |
|
|
|
|
return managedDependencies; |
|
|
|
|
} |
|
|
|
|
return managedDependencies; |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|