refactor of dependency-export plugin to allow for cli argument configuration

This commit is contained in:
2021-02-08 14:19:53 +01:00
parent 6f7ddc42ce
commit 2ff8c958c1
24 changed files with 537 additions and 447 deletions

30
build.gradle Normal file
View File

@@ -0,0 +1,30 @@
allprojects {
apply plugin: 'java-library'
repositories {
mavenLocal()
mavenCentral()
jcenter()
}
group = "net.woggioni.gradle"
version = 0.1
dependencies {
['compileOnly', 'annotationProcessor', 'testCompileOnly', 'testAnnotationProcessor'].each { conf ->
add(conf, [group: "org.projectlombok", name: "lombok", version: project['version.lombok']])
}
add("testImplementation", create(group: "org.junit.jupiter", name:"junit-jupiter-api", version: project["version.junitJupiter"]))
add("testRuntimeOnly", create(group: "org.junit.jupiter", name: "junit-jupiter-engine", version: project["version.junitJupiter"]))
add("testImplementation", gradleTestKit())
}
tasks.named("test", Test) {
useJUnitPlatform()
}
}
wrapper {
gradleVersion = "6.8"
distributionType = Wrapper.DistributionType.ALL
}

View File

@@ -1,26 +0,0 @@
allprojects {
apply<JavaLibraryPlugin>()
repositories {
mavenLocal()
mavenCentral()
jcenter()
}
group = "net.woggioni.plugins"
version = 0.1
dependencies {
add("testImplementation", create(group="org.junit.jupiter", name="junit-jupiter-api", version=project["version.junitJupiter"]))
add("testRuntimeOnly", create(group="org.junit.jupiter", name="junit-jupiter-engine", version=project["version.junitJupiter"]))
add("testImplementation", gradleTestKit())
}
tasks.named<Test>("test") {
useJUnitPlatform()
}
}
tasks.withType<Wrapper>().configureEach {
gradleVersion = "6.7"
distributionType = Wrapper.DistributionType.ALL
}

View File

@@ -36,14 +36,12 @@ plugins {
} }
``` ```
You can also enable it globally on your machine just create a `~/.gradle/init.gradle` file with this content You can also enable it globally on your machine, just create a `~/.gradle/init.gradle` file with this content
```groovy ```groovy
initscript { initscript {
repositories { repositories {
mavenLocal() mavenLocal()
mavenCentral()
jcenter()
} }
dependencies { dependencies {
classpath "net.woggioni.plugins:dependency-export:0.1" classpath "net.woggioni.plugins:dependency-export:0.1"
@@ -82,18 +80,15 @@ renderDependencies {
and using Kotlin DSL and using Kotlin DSL
```kotlin ```kotlin
import net.woggioni.plugins.ExportDependenciesPluginExtension tasks.named<net.woggioni.gradle.dependency.export.ExportDependencies>("exportDependencies") {
import net.woggioni.plugins.RenderDependenciesPluginExtension configurationName.set("compileClasspath")
outputFile.set(project.file("dependencies.dot"))
configure<ExportDependenciesPluginExtension> {
configurationName = "default"
outputFile = "dependencies.dot"
} }
configure<RenderDependenciesPluginExtension> { tasks.named<net.woggioni.gradle.dependency.export.RenderDependencies>("renderDependencies") {
format = "xlib" format.set("xlib")
outputFile = "renderedDependencies" outputFile.set(project.file("renderedDependencies"))
graphvizExecutable = "graphviz" graphvizExecutable.set("graphviz")
} }
``` ```
@@ -102,24 +97,30 @@ or using its correspondent Gradle's [project properties](https://docs.gradle.org
For example to override the output format of the `renderDependencies` task from the CLI: For example to override the output format of the `renderDependencies` task from the CLI:
```bash ```bash
gradle -PrenderDependencies.format=svg renderDependencies gradle exportDependencies --configuration=compileClasspath renderDependencies --format=svg
``` ```
### Parameter description ### Parameter description
- `exportDependencies.configurationName` will select the Gradle's configuration #### Attributes of task `net.woggioni.gradle.dependency.export.ExportDependencies`
(that word you put in the `dependencies` section of your build before `groupId:artifactId:versionId` tuple) - `configurationName` will select the Gradle's configuration
that will be represented in the graph (that word you put in the `dependencies` section of your build before `groupId:artifactId:versionId` tuple)
- `exportDependencies.outputFile` will specify the location of the generated `.dot` file that will be represented in the graph. It can also be specified from CLI using `--configuration`.
(note that if a relative path is provided, it will be interpreted as relative to the project's build directory) - `outputFile` will specify the location of the generated `.dot` file
- `renderDependencies.format` will specify the format of the file generated by Graphviz. (note that if a relative path is provided, it will be interpreted as relative to the project's build directory).
It can also be specified from CLI using `--output`.
#### Attributes of task `net.woggioni.gradle.dependency.export.RenderDependencies`
- `format` will specify the format of the file generated by Graphviz.
The default output format is `xlib` which, on a linux machine with a GUI, will open The default output format is `xlib` which, on a linux machine with a GUI, will open
a window with an interactive (with zoom and pan) view of your dependencies (this is a special format that a window with an interactive (with zoom and pan) view of your dependencies (this is a special format that
will not output any file). Otherwise you can choose between any other output format supported by Graphviz, will not output any file). Otherwise you can choose between any other output format supported by Graphviz,
refer to [its official documentation](https://graphviz.gitlab.io/_pages/doc/info/output.html) for more details. refer to [its official documentation](https://graphviz.gitlab.io/_pages/doc/info/output.html) for more details.
- `renderDependencies.outputFile` will specify the location of the generated file (note that if a It can also be specified from CLI using `--format`.
relative path is provided, it will be interpreted as relative to the project's build directory) - `outputFile` will specify the location of the generated file (note that if a
- `renderDependencies.graphvizExecutable` will set the executable that will be launched to invoke relative path is provided, it will be interpreted as relative to the project's build directory).
It can also be specified from CLI using `--output`.
- `graphvizExecutable` will set the executable that will be launched to invoke
Graphviz so that, if you have it installed in an exotic location outside of your `PATH` or, for Graphviz so that, if you have it installed in an exotic location outside of your `PATH` or, for
any reason, you renamed it in some way, you can configure it here. any reason, you renamed it in some way, you can configure it here.

View File

@@ -1,16 +1,10 @@
plugins { plugins {
`java-gradle-plugin` `java-gradle-plugin`
`maven-publish` `maven-publish`
id("org.jetbrains.kotlin.jvm")
id("com.gradle.plugin-publish") id("com.gradle.plugin-publish")
} }
dependencies { dependencies {
// Align versions of all Kotlin components
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
// Use the Kotlin JDK 8 standard library.
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
} }
java { java {
@@ -27,19 +21,8 @@ arrayOf("apiElements", "runtimeElements").forEach { name : String ->
gradlePlugin { gradlePlugin {
plugins { val dependencyExportPlugin by plugins.creating {
create("DependencyExportPlugin") { id = "net.woggioni.gradle.dependency-export"
id = "net.woggioni.plugins.dependency-export" implementationClass = "net.woggioni.gradle.dependency.export.DependencyExportPlugin"
implementationClass = "net.woggioni.plugins.dependency.export.DependencyExportPlugin"
}
} }
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
languageVersion = "1.3"
apiVersion = "1.3"
jvmTarget = "1.8"
javaParameters = true // Useful for reflection.
}
}

View File

@@ -4,13 +4,12 @@ buildscript {
} }
dependencies { dependencies {
classpath "net.woggioni.plugins:dependency-export:0.1" classpath "net.woggioni.gradle:dependency-export:0.1"
} }
} }
plugins { plugins {
id "org.jetbrains.kotlin.jvm" version "1.3.61" id "net.woggioni.gradle.dependency-export" version "0.1"
id "net.woggioni.plugins.dependency-export" version "0.1"
} }
repositories { repositories {
@@ -18,9 +17,8 @@ repositories {
mavenLocal() mavenLocal()
} }
exportDependencies { exportDependencies {
configurationName = 'runtime' configurationName = 'runtimeCl'
} }
renderDependencies { renderDependencies {

View File

@@ -5,16 +5,12 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("net.woggioni.plugins:dependency-export:0.1") classpath("net.woggioni.gradle:dependency-export:0.1")
} }
} }
import net.woggioni.plugins.ExportDependenciesPluginExtension
import net.woggioni.plugins.RenderDependenciesPluginExtension
plugins { plugins {
kotlin("jvm") version "1.3.71" id("net.woggioni.gradle.dependency-export") version "0.1"
id("net.woggioni.plugins.dependency-export") version "0.1"
} }
repositories { repositories {

View File

@@ -0,0 +1,32 @@
package net.woggioni.gradle.dependency.export;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.ObjectConfigurationAction;
import org.gradle.api.provider.Provider;
public class DependencyExportPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.apply(new Action<ObjectConfigurationAction>() {
@Override
public void execute(ObjectConfigurationAction objectConfigurationAction) {
objectConfigurationAction.plugin(JavaBasePlugin.class);
}
});
Provider<ExportDependencies> exportDependenciesTask =
project.getTasks().register("exportDependencies", ExportDependencies.class);
Provider<RenderDependencies> renderDependenciesTask =
project.getTasks().register("renderDependencies", RenderDependencies.class, new Action<RenderDependencies>() {
@Override
public void execute(RenderDependencies renderDependencies) {
renderDependencies.setExportTask(exportDependenciesTask);
}
});
// project.getExtensions().getExtraProperties().set(ExportDependencies.class.getSimpleName(), ExportDependencies.class);
project.getExtensions().getExtraProperties().set(RenderDependencies.class.getSimpleName(), RenderDependencies.class);
}
}

View File

@@ -0,0 +1,204 @@
package net.woggioni.gradle.dependency.export;
import lombok.*;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ResolvedArtifact;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.component.ProjectComponentIdentifier;
import org.gradle.api.artifacts.result.*;
import org.gradle.api.file.FileCollection;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.options.Option;
import javax.inject.Inject;
import java.io.File;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ExportDependencies extends DefaultTask {
@Getter
@Setter
@Input
private Property<String> configurationName;
@Getter
@Setter
@OutputFile
private Property<File> outputFile;
@Getter
@Setter
@Input
private Property<Boolean> showArtifacts;
private final JavaPluginConvention javaPluginConvention;
@InputFiles
public Provider<FileCollection> getConfigurationFiles() {
return configurationName.flatMap(name -> getProject().getConfigurations().named(name));
}
@Option(option = "configuration", description = "Set the configuration name")
public void setConfiguration(String configurationName) {
this.configurationName.set(configurationName);
}
@Option(option = "output", description = "Set the output file name")
public void setOutput(String outputFile) {
this.outputFile.set(getProject().file(outputFile));
}
@Option(option = "showArtifacts", description = "Show artifacts")
public void setArtifacts(boolean value) {
showArtifacts.set(value);
}
@Inject
public ExportDependencies(ObjectFactory objects) {
javaPluginConvention = getProject().getConvention().getPlugin(JavaPluginConvention.class);
configurationName = objects.property(String.class).convention("runtimeClasspath");
outputFile = objects.property(File.class).convention(
getProject().provider(() -> new File(javaPluginConvention.getDocsDir(), "dependencies.dot")));
showArtifacts = objects.property(Boolean.class).convention(false);
}
private static String quote(String s) {
return "\"" + s + "\"";
}
@TaskAction
@SneakyThrows
void run() {
Map<ResolvedComponentResult, Integer> map = new HashMap<>();
Configuration requestedConfiguration = Optional.ofNullable(getProject().getConfigurations().named(configurationName.get()).getOrNull()).orElseThrow(() -> {
String resolvableConfigurations = '[' + getProject().getConfigurations().stream()
.filter(Configuration::isCanBeResolved)
.map(it -> '\'' + it.getName() + '\'')
.collect(Collectors.joining(", ")) + ']';
throw new GradleException(String.format("Configuration '%s' doesn't exist or cannot be resolved, " +
"resolvable configurations in this project are %s", configurationName.get(), resolvableConfigurations));
});
ResolutionResult resolutionResult = requestedConfiguration.getIncoming().getResolutionResult();
Path destination = outputFile.map(it -> {
if (it.isAbsolute()) {
return it;
} else {
return new File(javaPluginConvention.getDocsDir(), it.toString());
}
}).map(File::toPath).get();
Files.createDirectories(destination.getParent());
try(Writer writer = Files.newBufferedWriter(destination)) {
writer.write("digraph G {");
writer.write('\n');
writer.write(" #rankdir=\"LR\";");
writer.write('\n');
Optional<Map<ComponentIdentifier, List<ResolvedArtifact>>> artifactMap = Optional.empty();
if(showArtifacts.get()) {
artifactMap = Optional.of(requestedConfiguration.getResolvedConfiguration().getResolvedArtifacts().stream().map(it ->
new AbstractMap.SimpleEntry<>(it.getId().getComponentIdentifier(), it)
).collect(Collectors.groupingBy(Map.Entry::getKey,
Collector.of(ArrayList::new,
(list, entry) -> list.add(entry.getValue()),
(l1, l2) -> { l1.addAll(l2); return l1; }))));
}
final int[] sequence = new int[1];
for (ResolvedComponentResult component : resolutionResult.getAllComponents()) {
map.computeIfAbsent(component, it -> sequence[0]++);
ComponentIdentifier id = component.getId();
Optional<List<ResolvedArtifact>> artifacts = artifactMap
.flatMap(it -> Optional.ofNullable(it.get(id)));
String componentName = id.getDisplayName();
String label = artifacts.map(it -> {
String rows = it.stream().map(resolvedArtifact -> {
String artifactDescription = Stream.of(
new AbstractMap.SimpleEntry<>("type", resolvedArtifact.getType()),
new AbstractMap.SimpleEntry<>("classifier", resolvedArtifact.getClassifier()),
new AbstractMap.SimpleEntry<>("extension",
!Objects.equals(resolvedArtifact.getExtension(), resolvedArtifact.getType()) ?
resolvedArtifact.getExtension() : null)
).map(entry -> {
if (entry.getValue() == null || entry.getValue().isEmpty()) return null;
else return entry.getKey() + ": " + entry.getValue();
}).collect(Collectors.joining(", "));
return "<TR><TD BGCOLOR=\"lightgrey\">" + artifactDescription + "</TD></TR>";
}).collect(Collectors.joining());
return "<<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"2\">" +
" <TR>" +
" <TD>" + id.getDisplayName() + "</TD>" +
" </TR>" +
" " + rows +
"</TABLE>>";
}).orElse(quote(componentName));
String shape;
String color;
if(id instanceof ProjectComponentIdentifier) {
shape = artifacts.isEmpty() ? "box" : "none";
color = "#88ff88";
} else if(id instanceof ModuleComponentIdentifier) {
shape = artifacts.isEmpty() ? "oval" : "none";
color = "#ffff88";
} else {
throw new IllegalArgumentException(id.getClass().getName());
}
Map<String, String> attrs = Stream.of(
new AbstractMap.SimpleEntry<>("label", label),
new AbstractMap.SimpleEntry<>("shape", quote(shape)),
new AbstractMap.SimpleEntry<>("style", quote("filled")),
artifacts.map(it -> new AbstractMap.SimpleEntry<>("margin", quote("0"))).orElse(null),
new AbstractMap.SimpleEntry<>("fillcolor", quote(color))
).filter(Objects::nonNull).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
writer.write(" node_" + map.get(component) + " [" +
attrs.entrySet().stream()
.map(it -> it.getKey() + '=' + it.getValue())
.collect(Collectors.joining(", ")) +
"];");
writer.write('\n');
}
@EqualsAndHashCode
@RequiredArgsConstructor
class Link {
final ComponentIdentifier id1;
final ComponentIdentifier id2;
}
Set<Link> linkCache = new HashSet<>();
for (ResolvedComponentResult component : resolutionResult.getAllComponents()) {
for(DependencyResult dependency : component.getDependencies()) {
if(dependency instanceof ResolvedDependencyResult) {
ResolvedComponentResult child =
((ResolvedDependencyResult) dependency).getSelected();
if(linkCache.add(new Link(component.getId(), child.getId()))) {
writer.write(" node_" + map.get(component) + " -> node_" + map.get(child) + ";");
writer.write('\n');
}
} else if(dependency instanceof UnresolvedDependencyResult) {
throw ((UnresolvedDependencyResult) dependency).getFailure();
} else {
throw new IllegalArgumentException(dependency.getClass().getName());
}
}
}
writer.write('}');
writer.write('\n');
}
}
}

View File

@@ -0,0 +1,87 @@
package net.woggioni.gradle.dependency.export;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.options.Option;
import javax.inject.Inject;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
public class RenderDependencies extends DefaultTask {
@Getter
@InputFile
private Provider<File> sourceFile;
@Getter
@Setter
private Property<String> format;
@Getter
@Setter
private Property<String> graphvizExecutable;
@Getter
@Setter
private Property<File> outputFile;
private final JavaPluginConvention javaPluginConvention;
@Option(option = "output", description = "Set the output file name")
public void setOutputCli(String outputFile) {
this.outputFile.set(getProject().file(outputFile));
}
@Option(option = "format", description = "Set output format (see https://graphviz.org/doc/info/output.html)")
public void setFormatCli(String format) {
this.format.set(format);
}
public void setExportTask(Provider<ExportDependencies> taskProvider) {
sourceFile = taskProvider.flatMap(ExportDependencies::getOutputFile);
}
@Inject
public RenderDependencies(ObjectFactory objects) {
sourceFile = objects.property(File.class);
javaPluginConvention = getProject().getConvention().getPlugin(JavaPluginConvention.class);
format = objects.property(String.class).convention("xlib");
graphvizExecutable = objects.property(String.class).convention("dot");
outputFile = objects.property(File.class)
.convention(new File(javaPluginConvention.getDocsDir(), "renderedDependencies"));
}
@TaskAction
@SneakyThrows
void run() {
Path destination = outputFile.map(it -> {
if (it.isAbsolute()) {
return it;
} else {
return new File(javaPluginConvention.getDocsDir(), it.toString());
}
}).map(File::toPath).get();
List<String> cmd = Arrays.asList(
graphvizExecutable.get(),
"-T" + format.get(),
"-o" + destination.toString(),
sourceFile.get().toString()
);
int returnCode = new ProcessBuilder(cmd).inheritIO().start().waitFor();
if (returnCode != 0) {
throw new GradleException("Error invoking graphviz");
}
}
}

View File

@@ -1,220 +0,0 @@
package net.woggioni.plugins.dependency.export
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.artifacts.result.ResolvedComponentResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.artifacts.result.UnresolvedDependencyResult
import java.io.BufferedWriter
import java.io.OutputStreamWriter
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.reflect.KMutableProperty0
open class ExportDependenciesPluginExtension(project: Project) {
var configurationName: String = "default"
var outputFile = project.buildDir.toPath().resolve("dependencies.dot")
var showArtifacts = false
}
open class RenderDependenciesPluginExtension(project: Project) {
var format: String = "xlib"
var outputFile = project.buildDir.toPath().resolve("renderedDependencies")
var graphvizExecutable: String = "dot"
}
private class Overrider(private val properties: Map<String, Any?>, private val prefix: String) {
inline fun <reified T> identity(arg: T): T {
return arg
}
fun overrideProperty(property: KMutableProperty0<String>) {
overrideProperty(property, ::identity)
}
inline fun <reified V> overrideProperty(
property: KMutableProperty0<V>,
valueFactory: (String) -> V) {
val propertyKey = prefix + "." + property.name
val propertyValue = properties[propertyKey] as String?
if (propertyValue != null) {
property.set(valueFactory(propertyValue))
}
}
}
object DependencyExporter {
private fun quote(s : String) = "\"" + s + "\""
fun exportDot(project: Project, ext: ExportDependenciesPluginExtension) {
val overrider = Overrider(project.properties, "exportDependencies")
overrider.overrideProperty(ext::configurationName)
overrider.overrideProperty(ext::outputFile) { value -> Paths.get(value) }
overrider.overrideProperty(ext::showArtifacts) { value -> value.toBoolean() }
var sequence = 0
val map = HashMap<ResolvedComponentResult, Int>()
val requestedConfiguration = project.configurations.singleOrNull {
it.name == ext.configurationName
}?.takeIf { it.isCanBeResolved } ?: let {
val resolvableConfigurations = "[" + project.configurations.asSequence()
.filter { it.isCanBeResolved }
.map { "'${it.name}'" }
.joinToString(",") + "]"
throw GradleException("Configuration '${ext.configurationName}' doesn't exist or cannot be resolved, " +
"resolvable configurations in this project are " + resolvableConfigurations)
}
val resolutionResult = requestedConfiguration.incoming.resolutionResult
if (!ext.outputFile.isAbsolute) {
ext.outputFile = project.buildDir.toPath().resolve(ext.outputFile)
}
Files.createDirectories(ext.outputFile.parent)
BufferedWriter(
OutputStreamWriter(
Files.newOutputStream(ext.outputFile))).use { writer ->
writer.write("digraph G {")
writer.newLine()
writer.write(" #rankdir=\"LR\";")
writer.newLine()
val artifactMap = if(ext.showArtifacts) {
requestedConfiguration.resolvedConfiguration.resolvedArtifacts.asSequence().map {
it.id.componentIdentifier to it
}.groupBy(
Pair<ComponentIdentifier, ResolvedArtifact>::first,
Pair<ComponentIdentifier, ResolvedArtifact>::second
)
} else {
null
}
for (component in resolutionResult.allComponents) {
map.computeIfAbsent(component) {
sequence++
}
val artifacts = artifactMap?.let { it[component.id] }
val (shape, color) = when (component.id) {
is ProjectComponentIdentifier -> (artifacts?.let { "none" } ?: "box") to "#88ff88"
is ModuleComponentIdentifier -> (artifacts?.let { "none" } ?: "oval") to "#ffff88"
else -> throw NotImplementedError("${component.id::class}")
}
val componentName = component.id.displayName
val label = artifacts?.let {
val rows = it.asSequence().map { resolvedArtifact ->
val artifactDescription = sequenceOf(
"type" to resolvedArtifact.type,
"classifier" to resolvedArtifact.classifier,
"extension" to resolvedArtifact.extension.takeIf {
resolvedArtifact.extension != resolvedArtifact.type
}
).mapNotNull { pair ->
when {
pair.second == null || pair.second.isEmpty() -> null
else -> "${pair.first}: ${pair.second}"
}
}.joinToString(", ")
"<TR><TD BGCOLOR=\"lightgrey\">$artifactDescription</TD></TR>"
}.joinToString()
"""
<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="2">
<TR>
<TD>${component.id.displayName}</TD>
</TR>
$rows
</TABLE>>
""".trimIndent()
} ?: quote(componentName)
val attrs = sequenceOf(
"label" to label,
"shape" to quote(shape),
"style" to quote("filled"),
artifacts?.let { "margin" to quote(0.toString()) },
"fillcolor" to quote(color)
).mapNotNull { it }.toMap()
writer.write(" node_${map[component]} [" +
attrs.entries
.asSequence()
.map { "${it.key}=${it.value}" }.joinToString(", ") +
"];")
writer.newLine()
}
for (component in resolutionResult.allComponents) {
component.dependencies.map { dependency ->
when (dependency) {
is ResolvedDependencyResult -> dependency
is UnresolvedDependencyResult -> {
throw dependency.failure
}
else -> {
throw NotImplementedError("${dependency::class}")
}
}
}.map(ResolvedDependencyResult::getSelected).forEach { child ->
writer.write(" node_${map[component]} -> node_${map[child]};")
writer.newLine()
}
}
writer.write("}")
writer.newLine()
}
}
}
object DependencyRenderer {
fun render(project: Project, ext: RenderDependenciesPluginExtension, sourceFile: Path) {
val overrider = Overrider(project.properties, "renderDependencies")
overrider.overrideProperty(ext::format)
overrider.overrideProperty(ext::graphvizExecutable)
overrider.overrideProperty(ext::outputFile) { value -> Paths.get(value) }
if (!ext.outputFile.isAbsolute) {
ext.outputFile = project.buildDir.toPath().resolve(ext.outputFile)
}
val cmd: List<String> = listOf(
ext.graphvizExecutable,
"-T${ext.format}",
"-o${ext.outputFile}",
sourceFile.toString()
)
val returnCode = ProcessBuilder(cmd).inheritIO().start().waitFor()
if (returnCode != 0) {
throw GradleException("Error invoking graphviz")
}
}
}
class DependencyExportPlugin : Plugin<Project> {
override fun apply(project: Project) {
val dependencyExportExtension = ExportDependenciesPluginExtension(project)
project.extensions.add(ExportDependenciesPluginExtension::class.java, "exportDependencies", dependencyExportExtension)
val exportDependenciesTask = project.tasks.register("exportDependencies") {
it.doLast {
DependencyExporter.exportDot(project, dependencyExportExtension)
}
}.get()
val renderDependenciesPluginExtension = RenderDependenciesPluginExtension(project)
project.extensions.add(RenderDependenciesPluginExtension::class.java, "renderDependencies", renderDependenciesPluginExtension)
project.tasks.register("renderDependencies") {
it.dependsOn(exportDependenciesTask)
it.doLast {
DependencyRenderer.render(project, renderDependenciesPluginExtension, dependencyExportExtension.outputFile)
}
}.get()
}
}

View File

@@ -0,0 +1,93 @@
package net.woggioni.gradle.dependency.export;
import lombok.SneakyThrows;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.stream.Stream;
public class DependencyExportPluginTest {
private static InputStream resourceFromClass(Class<?> cls, String resourceName) {
return cls.getResourceAsStream(resourceName);
}
private static InputStream resourceFromClassLoader(Class<?> cls, String resourceName) {
return cls.getClassLoader().getResourceAsStream(resourceName);
}
@SneakyThrows
private static void installResource(Class<?> cls, String resourceName, Path destination) {
Path outputFile;
Path realDestination;
if (Files.isSymbolicLink(destination)) {
realDestination = destination.toRealPath();
} else {
realDestination = destination;
}
if(!Files.exists(realDestination)) {
Files.createDirectories(realDestination.getParent());
outputFile = realDestination;
} else if(Files.isDirectory(realDestination)) {
outputFile = realDestination.resolve(resourceName.substring(1 + resourceName.lastIndexOf('/')));
} else if(Files.isRegularFile(realDestination)) {
outputFile = realDestination;
} else throw new IllegalStateException("Path '${realDestination}' is neither a file nor a directory");
Optional<InputStream> inputStreamOptional = Stream.<BiFunction<Class<?>, String, InputStream>>of(
DependencyExportPluginTest::resourceFromClass,
DependencyExportPluginTest::resourceFromClassLoader
).map(f -> f.apply(cls, resourceName)).filter(Objects::nonNull).findFirst();
try(InputStream inputStream = inputStreamOptional.orElseThrow(() -> new FileNotFoundException(resourceName))){
Files.copy(inputStream, outputFile, StandardCopyOption.REPLACE_EXISTING);
}
}
@TempDir
public Path testGradleHomeDir;
@TempDir
public Path testProjectDir;
public Path buildFile;
@BeforeEach
public void setup() {
buildFile = testProjectDir.resolve("build.gradle.kts");
}
public GradleRunner getStandardGradleRunnerFor(String taskName) {
return GradleRunner.create()
.withDebug(true)
.withProjectDir(testProjectDir.toFile())
.withArguments(taskName, "-s", "--info", "-g", testGradleHomeDir.toString())
.withPluginClasspath();
}
@Test
public void testKotlin() {
installResource(getClass(), "build.gradle.kts", testProjectDir);
installResource(getClass(),"settings.gradle.kts", testProjectDir);
installResource(getClass(),"gradle.properties", testProjectDir);
GradleRunner runner = getStandardGradleRunnerFor("exportDependencies");
runner.build();
}
@Test
public void testGroovy() {
installResource(getClass(),"build.gradle", testProjectDir);
installResource(getClass(),"settings.gradle.kts", testProjectDir);
installResource(getClass(),"gradle.properties", testProjectDir);
GradleRunner runner = getStandardGradleRunnerFor("exportDependencies");
runner.build();
}
}

View File

@@ -1,52 +0,0 @@
/*
* This Kotlin source file was generated by the Gradle 'init' task.
*/
package net.woggioni.plugins.dependency.export
import org.gradle.testkit.runner.GradleRunner
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
class DependencyExportPluginTest {
@TempDir
lateinit var testGradleHomeDir : Path
@TempDir
lateinit var testProjectDir : Path
lateinit var buildFile : Path
@BeforeEach
fun setup() {
buildFile = testProjectDir.resolve("build.gradle.kts")
}
fun getStandardGradleRunnerFor(taskName: String): GradleRunner {
return GradleRunner.create()
.withDebug(true)
.withProjectDir(testProjectDir.toFile())
.withArguments(taskName, "-s", "--info", "-g", testGradleHomeDir.toString())
.withPluginClasspath()
}
@Test
fun testKotlin() {
installResource("build.gradle.kts", testProjectDir)
installResource("settings.gradle.kts", testProjectDir)
installResource("gradle.properties", testProjectDir)
val runner = getStandardGradleRunnerFor("exportDependencies")
runner.build()
}
@Test
fun testGroovy() {
installResource("build.gradle", testProjectDir)
installResource("settings.gradle.kts", testProjectDir)
installResource("gradle.properties", testProjectDir)
val runner = getStandardGradleRunnerFor("exportDependencies")
runner.build()
}
}

View File

@@ -1,33 +0,0 @@
package net.woggioni.plugins.dependency.export
import java.io.FileNotFoundException
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
@Throws(IOException::class)
fun Any.installResource(resourceName: String, destination: Path) {
val outputFile = run {
val realDestination = if (Files.isSymbolicLink(destination)) {
destination.toRealPath()
} else {
destination
}
when {
!Files.exists(realDestination) -> {
Files.createDirectories(realDestination.parent)
realDestination
}
Files.isDirectory(realDestination) ->
realDestination.resolve(resourceName.substring(1 + resourceName.lastIndexOf('/')))
Files.isRegularFile(realDestination) -> realDestination
else -> throw IllegalStateException("Path '${realDestination}' is neither a file nor a directory")
}
}
(javaClass.getResourceAsStream(resourceName)
?: javaClass.classLoader.getResourceAsStream(resourceName))?.use { inputStream ->
Files.copy(inputStream, outputFile, StandardCopyOption.REPLACE_EXISTING)
} ?: throw FileNotFoundException(resourceName)
}

View File

@@ -0,0 +1,18 @@
plugins {
id 'java-library'
id "net.woggioni.gradle.dependency-export"
}
exportDependencies {
configurationName = 'compileClasspath'
}
repositories {
jcenter()
mavenLocal()
}
dependencies {
runtimeOnly("org.hibernate:hibernate-core:5.4.13.Final")
}

View File

@@ -0,0 +1,17 @@
plugins {
id("java-library")
id("net.woggioni.gradle.dependency-export")
}
repositories {
jcenter()
mavenLocal()
}
dependencies {
runtimeOnly("org.hibernate:hibernate-core:5.4.13.Final")
}
tasks.named<net.woggioni.gradle.dependency.export.ExportDependencies>("exportDependencies") {
configurationName.set("compileClasspath")
}

View File

@@ -1,17 +0,0 @@
plugins {
id "org.jetbrains.kotlin.jvm" version "1.3.61"
id "net.woggioni.plugins.dependency-export"
}
exportDependencies {
configurationName = 'runtime'
}
repositories {
jcenter()
mavenLocal()
}
dependencies {
runtime("org.hibernate:hibernate-core:5.4.13.Final")
}

View File

@@ -1,14 +0,0 @@
plugins {
kotlin("jvm") version "1.3.71"
id("net.woggioni.plugins.dependency-export")
}
repositories {
jcenter()
mavenLocal()
}
dependencies {
runtime("org.hibernate:hibernate-core:5.4.13.Final")
}

View File

@@ -1,2 +1,6 @@
version.kotlin=1.3.72
version.gradlePublish=0.10.1
version.lombok=1.18.16
version.slf4j=1.7.30
version.junitJupiter=5.7.0 version.junitJupiter=5.7.0
version.junitPlatform=1.7.0 version.junitPlatform=1.7.0

Binary file not shown.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

2
gradlew vendored
View File

@@ -130,7 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"` APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"` JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath # We build the pattern for arguments to be converted via cygpath

21
gradlew.bat vendored
View File

@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,21 +64,6 @@ echo location of your Java installation.
goto fail goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
@@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

15
settings.gradle Normal file
View File

@@ -0,0 +1,15 @@
pluginManagement {
repositories {
gradlePluginPortal()
}
plugins {
id 'org.jetbrains.kotlin.jvm' version this['version.kotlin']
id 'com.gradle.plugin-publish' version this['version.gradlePublish']
}
}
rootProject.name = "my-gradle-plugins"
include("dependency-export")
include("jpms-check")
include("multi-release-jar")

View File

@@ -1,11 +0,0 @@
pluginManagement {
plugins {
id("org.jetbrains.kotlin.jvm") version "1.3.72" apply false
id("com.gradle.plugin-publish") version "0.10.1" apply false
}
}
rootProject.name = "my-gradle-plugins"
include("dependency-export")
include("jpms-check")
include("multi-release-jar")