Last active
February 19, 2021 00:47
-
-
Save wdziemia/3cde231406f84295b43399bc53458d9e to your computer and use it in GitHub Desktop.
bintrayCheck.gradle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2021 Wojciech Dziemianczyk | |
* | |
* 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. | |
* | |
* | |
* Apply this file to your root build.gradle | |
* | |
* Groovy: apply from: "bintrayCheck.gradle" | |
* Kotlin: apply(from = "bintrayCheck.gradle") | |
* | |
* Tasks: | |
* bintrayBuildscriptCheck - Detect Bintray repos in the buildscript block | |
* bintrayModuleCheck - Detect Bintray repos in each sub module of this project | |
* | |
* Params: | |
* -Pforce - force the check even if no bintray repos are defined in the repositories block | |
*/ | |
import org.gradle.api.internal.artifacts.DefaultProjectComponentIdentifier | |
import org.gradle.api.internal.artifacts.dependencies.DefaultSelfResolvingDependency | |
import org.gradle.api.internal.artifacts.repositories.DefaultMavenArtifactRepository | |
import org.gradle.api.tasks.diagnostics.internal.dependencies.AsciiDependencyReportRenderer | |
import org.gradle.api.tasks.diagnostics.internal.graph.nodes.RenderableDependency | |
import org.gradle.api.tasks.diagnostics.internal.graph.nodes.RenderableModuleResult | |
import org.gradle.api.tasks.diagnostics.internal.graph.nodes.UnresolvableConfigurationResult | |
import org.gradle.internal.component.external.model.DefaultModuleComponentIdentifier | |
import org.gradle.internal.deprecation.DeprecatableConfiguration | |
import org.gradle.internal.logging.text.StyledTextOutput | |
import org.gradle.internal.logging.text.StyledTextOutputFactory | |
import sun.net.www.protocol.file.FileURLConnection | |
import static org.gradle.internal.logging.text.StyledTextOutput.Style | |
/** | |
* Root level BuildEnvironmentReportTask task which checks if any of the artifacts in | |
* `buildscript { repositroes {}}` block are from a bintray repositores | |
*/ | |
task bintrayBuildscriptCheck(type: BuildEnvironmentReportTask) { | |
// Create our "hook" which stores the list of artifacts | |
def rendererHook = new DependencyRendererHook() | |
setRenderer(rendererHook) | |
doLast { | |
println('\n') | |
def out = services.get(StyledTextOutputFactory).create("jCenter()") | |
def buildscriptRepos = (rootProject.buildscript.repositories).findAll { repo -> | |
repo instanceof MavenArtifactRepository | |
} | |
// Iterate through all the repos and pick out the ones that contain "bintray" in the url | |
def bintrayRepos = findBintrayRepos(buildscriptRepos) | |
if (!bintrayRepos.isEmpty() || isForce()) { | |
// Print the bintray repos | |
printOffendingRepos("Buildscript", bintrayRepos, out) | |
// If the bintray repos are placed at the end of the list, iterate through all the artifacts and see which | |
// artifacts still rely on bintray | |
if (!printMisorderedRepos(buildscriptRepos, bintrayRepos, out)) { | |
printOffendingArtifacts(buildscriptRepos, rendererHook, bintrayRepos, out) | |
} | |
} else { | |
out.withStyle(Style.SuccessHeader).println("Buildscript declares no bintray repo") | |
} | |
} | |
} | |
private List<ArtifactRepository> findBintrayRepos(List<DefaultMavenArtifactRepository> repositories) { | |
repositories.findAll { repo -> | |
repo.url.toString().contains("bintray") | |
} | |
} | |
private Boolean isForce(){ | |
return properties.containsKey("force") | |
} | |
subprojects { | |
task bintrayModuleCheck(type: DependencyReportTask) { | |
// Create our "hook" which stores the list of artifacts | |
def rendererHook = new DependencyRendererHook() | |
setRenderer(rendererHook) | |
doLast { | |
def out = services.get(StyledTextOutputFactory).create("jCenter()") | |
def projectRepos = project.repositories.findAll { repo -> | |
repo instanceof DefaultMavenArtifactRepository | |
} | |
// Iterate through all the repos and pick out the ones that contain "bintray" in the url | |
def bintrayRepos = findBintrayRepos(projectRepos) | |
if (!bintrayRepos.isEmpty() || isForce()) { | |
// Print the bintray repos | |
printOffendingRepos("Module '$project.name'", bintrayRepos, out) | |
// If the bintray repos are placed at the end of the list, iterate through all the artifacts and see which | |
// artifacts still rely on bintray | |
if (!printMisorderedRepos(projectRepos, bintrayRepos, out)) { | |
printOffendingArtifacts(projectRepos, rendererHook, bintrayRepos, out) | |
} | |
} else { | |
out.withStyle(Style.SuccessHeader).println("\n'$project.name' declares no bintray repo\n") | |
} | |
} | |
} | |
} | |
/** | |
* Iterates through the supplied bintray repos and prints the name and url of each | |
*/ | |
private void printOffendingRepos(String type, List<ArtifactRepository> bintrayRepos, StyledTextOutput out) { | |
out.withStyle(Style.FailureHeader).println("\n$type declares ${bintrayRepos.size()} bintray repo(s)") | |
bintrayRepos.forEach { repo -> | |
out.withStyle(Style.Failure).println(" * $repo.name $repo.url") | |
} | |
} | |
/** | |
* Checks to see if the bintray repos are declared at the end of the repositories block | |
* | |
* @return true if the bintray repos do not occur at the end of the repositories block | |
*/ | |
private Boolean printMisorderedRepos(List<ArtifactRepository> repositories, List<ArtifactRepository> bintrayRepos, StyledTextOutput out) { | |
def declaredRepoCount = repositories.size() | |
def bintrayStartIndex = declaredRepoCount - bintrayRepos.size() | |
for (int i = bintrayStartIndex; i < declaredRepoCount; i++) { | |
def repo = repositories.get(i) | |
if (bintrayRepos.find { bintrayRepo -> | |
repo instanceof DefaultMavenArtifactRepository && bintrayRepo.url.toString() == repo.url.toString() | |
} == null) { | |
out.withStyle(Style.Failure).println("\nBintray repositories should be placed after any other " + | |
"repositories (e.g. google(), mavenCentral(). If not, artifacts may be resolved from a " + | |
"bintray repository that already exist elsewhere.\n") | |
return true | |
} | |
} | |
return false | |
} | |
/** | |
* Iterates through all dependencies collected in DependencyRendererHook and checks if those artifacts are resolved | |
* in a bintray repository. | |
*/ | |
private void printOffendingArtifacts( | |
List<ArtifactRepository> repositories, | |
DependencyRendererHook rendererHook, | |
List<ArtifactRepository> bintrayRepos, | |
StyledTextOutput out | |
) { | |
def uniqueDependencies = new HashSet<DefaultModuleComponentIdentifier>(rendererHook.uniqueDependencies) | |
// Store a set of Artifacts per repo | |
def bintrayArtifactMap = new HashMap<String, HashSet<DefaultModuleComponentIdentifier>>() | |
bintrayRepos.forEach { bintrayRepo -> | |
bintrayArtifactMap.put(bintrayRepo.url.toString(), new HashSet<DefaultModuleComponentIdentifier>()) | |
} | |
// Iterate through all teh artifacts, generate a url, and execute a HEAD request to see if that repo contains | |
// the artifact | |
repositories.forEach { repo -> | |
def uniqueDependenciesCopy = new HashSet<DefaultModuleComponentIdentifier>(uniqueDependencies) | |
uniqueDependenciesCopy.forEach { artifact -> | |
def group = artifact.group | |
if (group.contains('.')) { | |
group = artifact.group.replace('.', '/') | |
} | |
// Some repo urls already end with a /, others don't, make sure the forward slash is consistent | |
String repoUrl = repo.url | |
if (!repoUrl.endsWith('/')){ | |
repoUrl += '/' | |
} | |
// Not all repo hosts will allow us to check the root path (google throws a 404). However, we know a pom | |
// file exists for each version so we use that to check. | |
def url = "$repoUrl${group}/$artifact.module/${artifact.version}/$artifact.module-${artifact.version}.pom" | |
def connection = new URL(url).openConnection() | |
if (repo.getCredentials() != null) { | |
String credentials = repo.getCredentials().getUsername() + ":" + repo.getCredentials().getPassword() | |
String basicAuth = "Basic " + new String(Base64.getEncoder().encode(credentials.getBytes())) | |
connection.setRequestProperty("Authorization", basicAuth) | |
} | |
// If request was successful, add it to our bintrayArtifactMap, remove the entry from uniqueDependencies | |
// so that the next repository doesn't check for the artifact. | |
def hasArtifact = false | |
if (connection instanceof FileURLConnection) { | |
hasArtifact = ((FileURLConnection) connection).file.exists() | |
} else { | |
def responseCode = ((HttpURLConnection) connection).getResponseCode() | |
hasArtifact = responseCode >= 200 && responseCode < 400 | |
} | |
if (hasArtifact) { | |
if (bintrayArtifactMap.containsKey(repo.url.toString())) { | |
bintrayArtifactMap.get(repo.url.toString()).add(artifact) | |
} | |
uniqueDependencies.remove(artifact) | |
} | |
} | |
} | |
// If we still have some leftover .. its probably indicative of using the plugins block | |
if (!uniqueDependencies.isEmpty()) { | |
out.withStyle(Style.FailureHeader).println("\n\n${uniqueDependencies.size()} artifacts did not resolve to any " + | |
"repo. This is probably due to the use of the plugins {} block, a repo behind a VPN, or the need for credentials.") | |
uniqueDependencies.forEach { artifact -> | |
out.withStyle(Style.Failure).println(" * $artifact.group:$artifact.module:$artifact.version") | |
} | |
println("\n") | |
} | |
// After all requests have been made, iterate through bintrayArtifactMap and print the artifacts that resolved to | |
// each bintray repository. Include the configurations that rely on that artifact | |
bintrayArtifactMap.forEach { repo, artifacts -> | |
if (artifacts.isEmpty()) { | |
out.withStyle(Style.SuccessHeader).println("\n0 artifacts resolved to bintray repository '$repo', it is safe to remove!") | |
} else { | |
out.withStyle(Style.FailureHeader).println("$repo resolved ${artifacts.size()} artifact(s)") | |
artifacts.forEach { artifact -> | |
def configs = rendererHook.artifactToConfigurationMap.get(artifact).join(",") | |
out.withStyle(Style.Failure).println(" * $artifact.group:$artifact.module:$artifact.version ($configs)") | |
} | |
} | |
} | |
} | |
/** | |
* Use AsciiDependencyReportRenderer as a sort of hook into collecting a unique set of artifacts (uniqueDependencies) | |
* along with a List of configurations that each artifact is used in (artifactToConfigurationMap) | |
*/ | |
class DependencyRendererHook extends AsciiDependencyReportRenderer { | |
def uniqueDependencies = new HashSet<DefaultModuleComponentIdentifier>() | |
def artifactToConfigurationMap = new HashMap<DefaultModuleComponentIdentifier, List<String>>() | |
@Override | |
void render(Configuration configuration) { | |
if (isResolveable(configuration)) { | |
ResolutionResult result = configuration.getIncoming().getResolutionResult(); | |
RenderableDependency root = new RenderableModuleResult(result.getRoot()); | |
root.children.forEach { dependency -> | |
extractDependencies(configuration, dependency) | |
} | |
} else { | |
new UnresolvableConfigurationResult(configuration).children.forEach { dependency -> | |
extractDependencies(configuration, dependency) | |
} | |
} | |
} | |
boolean isValidType(Object id){ | |
return !(id instanceof DefaultSelfResolvingDependency || id instanceof DefaultProjectComponentIdentifier) | |
} | |
void extractDependencies(Configuration configuration, RenderableDependency dependency) { | |
// Ignore a dependency if we already are keeping track of it | |
if (!uniqueDependencies.contains(dependency.id) && isValidType(dependency.id)) { | |
uniqueDependencies.add(dependency.id) | |
// Keep track of the configuration that dependency is used in | |
if (!artifactToConfigurationMap.containsKey(dependency.id)) { | |
def configurations = new ArrayList<String>() | |
configurations.add(configuration.name) | |
artifactToConfigurationMap.put(dependency.id, configurations) | |
} else { | |
artifactToConfigurationMap.get(dependency.id).add(configuration.name) | |
} | |
// Recursively go through the transitive dependencies | |
if (!dependency.children.isEmpty()) { | |
dependency.children.forEach { child -> | |
extractDependencies(configuration, child) | |
} | |
} | |
} | |
} | |
private boolean isResolveable(Configuration configuration) { | |
boolean isDeprecatedForResolving = ((DeprecatableConfiguration) configuration).getResolutionAlternatives() != null | |
return configuration.isCanBeResolved() && !isDeprecatedForResolving | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment