Skip to content

Instantly share code, notes, and snippets.

@wdziemia
Last active February 19, 2021 00:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wdziemia/3cde231406f84295b43399bc53458d9e to your computer and use it in GitHub Desktop.
Save wdziemia/3cde231406f84295b43399bc53458d9e to your computer and use it in GitHub Desktop.
bintrayCheck.gradle
/**
* 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