Skip to content

Commit

Permalink
Merge pull request #132 from jjohannes/gradle-6
Browse files Browse the repository at this point in the history
Use variant-aware dependency management
  • Loading branch information
sghill authored Feb 3, 2020
2 parents 7764bb2 + 32fef6c commit df6621b
Show file tree
Hide file tree
Showing 41 changed files with 2,008 additions and 874 deletions.
42 changes: 33 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
This is a Gradle plugin for building [Jenkins](http://jenkins-ci.org)
plugins, written in Groovy or Java.

## Compatibility with Gradle versions

The latest version of the JPI plugin requires **Gradle 6+** to make use of advanced dependency management features.

For Gradle versions 4.x or 5.x, please use version `0.38.0` of the JPI plugin.

## Configuration

Add the following to your build.gradle:
Expand Down Expand Up @@ -93,20 +99,38 @@ repositories are defined in your build.gradle.

## Dependencies on other Jenkins Plugins

If your plugin depends on other Jenkins plugins you can specify the dependencies in the following way:
If your plugin depends on other Jenkins plugins, you can use the same _configurations_ as in Gradle's `java-libary` plugin.
See [the documentation](https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_separation) for details on the difference of `api` and `implementation` dependencies.
For _optional dependencies_, you can use Gradle's [feature variants](https://docs.gradle.org/current/userguide/feature_variants.html).

You can define both dependencies to Jenkins plugins and plain Java libraries.
The JPI plugin will figure out what you are depending on and process it accordingly (Java libraries will be packaged in the your Jenkins plugin's hpi/jpi file).

The additional `jenkinsServer` configuration can be used to install extra plugins for the `server` task (see below).

Examples:

java {
// define features for 'optional dependencies'
registerFeature('ant') {
usingSourceSet(sourceSets.main)
}
}

dependencies {
jenkinsPlugins 'org.jenkinsci.plugins:git:1.1.15'
optionalJenkinsPlugins 'org.jenkins-ci.plugins:ant:1.2'
jenkinsTest 'org.jenkins-ci.main:maven-plugin:1.480'
implementation 'org.jenkinsci.plugins:git:1.1.15'
api 'org.jenkins-ci.plugins:credentials:1.9.4'
// dependency of the (optional) ant feature
antImplementation 'org.jenkins-ci.plugins:ant:1.2'
// dependency for testing only
testImplementation 'org.jenkins-ci.main:maven-plugin:1.480'
// addition dependencies for manual tests on the server started with `gradle server`
jenkinsServer 'org.jenkins-ci.plugins:ant:1.2'
}

Adding the dependency to the `jenkinsPlugins` configuration will make all classes available during compilation and
also add the dependency to the manifest of your plugin. To define an optional dependency on a plugin then use
the `optionalJenkinsPlugins` configuration and to use a plugin only for testing, add a dependency to the `jenkinsTest`
configuration.
`jenkinsServer` can be used to install extra plugins for the `server` task (see below)

## Usage

Expand Down
6 changes: 1 addition & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,10 @@ tasks.addRule("Pattern: testGradle<ID>") {
useJUnit {
includeCategories("org.jenkinsci.gradle.plugins.jpi.UsesGradleTestKit")
}
onlyIf {
gradleVersion.startsWith('4') && System.getProperty("java.specification.version") == "1.8" ||
gradleVersion.startsWith('5')
}
}
}

setOf("4.10.3", "5.6.4", "6.0.1")
setOf("6.0.1")
.map { tasks.named("testGradle$it") }
.forEach { tasks.check { dependsOn(it) } }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package org.jenkinsci.gradle.plugins.jpi

import groovy.transform.CompileStatic
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.artifacts.ModuleVersionIdentifier
import org.gradle.api.artifacts.result.DependencyResult
import org.gradle.api.artifacts.result.ResolvedComponentResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.artifacts.result.ResolvedVariantResult
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.LibraryElements

@CompileStatic
class DependencyAnalysis {

private class JpiConfigurations {
Configuration consumableLibraries
Configuration consumablePlugins
Configuration resolvablePlugins

JpiConfigurations(Configuration consumableLibraries,
Configuration consumablePlugins,
Configuration resolvablePlugins) {
this.consumableLibraries = consumableLibraries
this.consumablePlugins = consumablePlugins
this.resolvablePlugins = resolvablePlugins
}
}

final Configuration allLibraryDependencies

private static final Attribute CATEGORY_ATTRIBUTE =
Attribute.of(Category.CATEGORY_ATTRIBUTE.name, String)
private static final Attribute LIBRARY_ELEMENTS_ATTRIBUTE =
Attribute.of(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE.name, String)
private final List<JpiConfigurations> jpiConfigurations = []

private DependencyAnalysisResult analysisResult

DependencyAnalysis(Project project) {
this.allLibraryDependencies = project.configurations.detachedConfiguration()
this.allLibraryDependencies.withDependencies {
// do the analysis when this configuration is resolved
analyse()
}
}

void registerJpiConfigurations(Configuration consumableLibraries,
Configuration consumablePlugins,
Configuration resolvablePlugins) {
jpiConfigurations.add(new JpiConfigurations(consumableLibraries, consumablePlugins, resolvablePlugins))
}

DependencyAnalysisResult analyse() {
if (analysisResult) {
return analysisResult
}

def manifestEntry = new StringBuilder()

jpiConfigurations.each { confs ->
analyseDependencies(confs, allLibraryDependencies, manifestEntry)
}
analysisResult = new DependencyAnalysisResult(manifestEntry.toString())
analysisResult
}

private analyseDependencies(JpiConfigurations configurations,
Configuration allLibraries, StringBuilder manifestEntry) {
def optional = configurations.resolvablePlugins.name != JpiPlugin.JENKINS_RUNTIME_CLASSPATH_CONFIGURATION_NAME

List<ModuleVersionIdentifier> processedComponents = []
configurations.resolvablePlugins.incoming.resolutionResult.root.dependencies.each { DependencyResult result ->
def selected = getSelectedComponent(result, processedComponents)
selected?.variants?.each { variant ->
if (variant.attributes.getAttribute(CATEGORY_ATTRIBUTE) != Category.LIBRARY
|| variant.attributes.getAttribute(LIBRARY_ELEMENTS_ATTRIBUTE) != JpiPlugin.JPI) {
// Skip dependencies that are not libraries with JPI files.
// We request these in the setup in JpiPlugin.configureConfigurations().
// However, an individual dependency can override attributes, for example 'category=platform'.
return
}

def moduleVersion = selected.moduleVersion
if (isMainFeature(moduleVersion, variant)) {
addToManifestEntry(manifestEntry, selected, optional)
} else {
selected.getDependenciesForVariant(variant).each { featureDependency ->
// add dependencies of the selected optional feature
addToManifestEntry(manifestEntry,
getSelectedComponent(featureDependency, processedComponents), optional)
}
}

def moduleDependencies = configurations.resolvablePlugins.allDependencies.findAll {
it instanceof ModuleDependency && it.group == moduleVersion.group && it.name == moduleVersion.name
}
configurations.consumablePlugins.dependencies.addAll(moduleDependencies)
}
}
allLibraries.dependencies.addAll(configurations.consumableLibraries.allDependencies
- configurations.consumablePlugins.allDependencies)
}

private static ResolvedComponentResult getSelectedComponent(DependencyResult dependency,
List<ModuleVersionIdentifier> processedComponents) {
if (dependency.constraint || !(dependency instanceof ResolvedDependencyResult)) {
return null
}
def selected = ((ResolvedDependencyResult) dependency).selected
def moduleVersion = selected.moduleVersion
if (moduleVersion == null || processedComponents.contains(moduleVersion)) {
// If feature variants are used, it is common to have multiple dependencies to the same component.
// These then turn up in the result multiple times.
return null
}
processedComponents.add(moduleVersion)
selected
}

static boolean isMainFeature(ModuleVersionIdentifier component, ResolvedVariantResult variant) {
// either no capability definition of main capability is explicitly defined
variant.capabilities.isEmpty() || variant.capabilities.any {
it.group == component.group && it.name == component.name
}
}

private static void addToManifestEntry(StringBuilder manifestEntry,
ResolvedComponentResult selected,
boolean optional) {
if (selected) {
if (manifestEntry.length() > 0) {
manifestEntry.append(',')
}
manifestEntry.append(selected.moduleVersion.name)
manifestEntry.append(':')
manifestEntry.append(selected.moduleVersion.version)
if (optional) {
manifestEntry.append(';resolution:=optional')
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.jenkinsci.gradle.plugins.jpi

import groovy.transform.CompileStatic

@CompileStatic
class DependencyAnalysisResult {

final String manifestPluginDependencies

DependencyAnalysisResult(String manifestPluginDependencies) {
this.manifestPluginDependencies = manifestPluginDependencies
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,15 @@ import org.gradle.api.file.DirectoryProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.util.GradleVersion

class GenerateTestHpl extends DefaultTask {
public static final String TASK_NAME = 'generate-test-hpl'

private static final GradleVersion GRADLE_5_0 = GradleVersion.version('5.0')

@OutputDirectory
final DirectoryProperty hplDir

GenerateTestHpl() {
if (GradleVersion.current() >= GRADLE_5_0) {
this.hplDir = services.get(ObjectFactory).directoryProperty()
} else {
this.hplDir = newOutputDirectory()
}
this.hplDir = services.get(ObjectFactory).directoryProperty()
}

@TaskAction
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.jenkinsci.gradle.plugins.jpi

import hudson.util.VersionNumber
import org.gradle.api.artifacts.CacheableRule
import org.gradle.api.artifacts.ComponentMetadataContext
import org.gradle.api.artifacts.ComponentMetadataRule
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.model.ObjectFactory

import javax.inject.Inject

@CacheableRule
abstract class JenkinsWarRule implements ComponentMetadataRule {

static final JENKINS_WAR_COORDINATES = 'org.jenkins-ci.main:jenkins-war'

@Inject
abstract ObjectFactory getObjects()

/**
* A Jenkins 'war' or 'war-for-test' is required on the Jenkins test classpath. This classpath expects JPI
* variants. This rule adds such a variant to the Jenkins war module pointing at the right artifact depending
* on the version of the module.
*/
@Override
void execute(ComponentMetadataContext ctx) {
def id = ctx.details.id
ctx.details.addVariant('jenkinsTestRuntimeElements', 'runtime') {
it.attributes {
it.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
objects.named(LibraryElements, JpiPlugin.JPI))
}
it.withDependencies {
// Dependencies with a classifier point at JARs and can be removed
// TODO needs public API - https://github.com/gradle/gradle/issues/11975
it.removeAll { it.originalMetadata?.dependencyDescriptor?.dependencyArtifact?.classifier }
}
if (new VersionNumber(id.version) < new VersionNumber('2.64')) {
it.withFiles {
it.removeAllFiles()
it.addFile("${id.name}-${id.version}-war-for-test.jar")
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.SourceSet
import org.gradle.util.ConfigureUtil
import org.gradle.util.GradleVersion

/**
* This gets exposed to the project as 'jpi' to offer additional convenience methods.
Expand All @@ -32,6 +31,7 @@ import org.gradle.util.GradleVersion
*/
class JpiExtension {
final Project project
Map<String, String> jenkinsWarCoordinates

JpiExtension(Project project) {
this.project = project
Expand Down Expand Up @@ -163,27 +163,21 @@ class JpiExtension {

if (this.coreVersion) {
project.dependencies {
if (GradleVersion.current() >= GradleVersion.version('4.6')) {
annotationProcessor "org.jenkins-ci.main:jenkins-core:$v"
}
jenkinsCore(
jenkinsWarCoordinates = [group: 'org.jenkins-ci.main', name: 'jenkins-war', version: v]
testRuntimeOnly(jenkinsWarCoordinates)

annotationProcessor "org.jenkins-ci.main:jenkins-core:$v"

compileOnly(
[group: 'org.jenkins-ci.main', name: 'jenkins-core', version: v],
[group: findBugsGroup, name: 'annotations', version: findBugsVersion],
[group: 'javax.servlet', name: servletApiArtifact, version: servletApiVersion],
)

jenkinsWar(group: 'org.jenkins-ci.main', name: 'jenkins-war', version: v)

if (new VersionNumber(this.coreVersion) < new VersionNumber('2.64')) {
jenkinsTest("org.jenkins-ci.main:jenkins-war:${v}:war-for-test")
} else {
project.configurations.jenkinsTest.extendsFrom(project.configurations.jenkinsWar)
}

jenkinsTest("org.jenkins-ci.main:jenkins-test-harness:${testHarnessVersion}")
jenkinsTest("org.jenkins-ci.main:ui-samples-plugin:${uiSamplesVersion}")
testImplementation("org.jenkins-ci.main:jenkins-test-harness:${testHarnessVersion}")
testImplementation("org.jenkins-ci.main:ui-samples-plugin:${uiSamplesVersion}")
if (new VersionNumber(this.coreVersion) < new VersionNumber('1.505')) {
jenkinsTest('junit:junit-dep:4.10')
testImplementation('junit:junit-dep:4.10')
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
package org.jenkinsci.gradle.plugins.jpi

import org.gradle.api.Project
import org.gradle.api.plugins.WarPlugin
import org.gradle.api.plugins.WarPluginConvention
import org.gradle.api.tasks.bundling.War

/**
* @author Kohsuke Kawaguchi
Expand All @@ -27,15 +24,13 @@ class JpiHplManifest extends JpiManifest {
JpiHplManifest(Project project) {
super(project)

def conv = project.extensions.getByType(JpiExtension)
War war = project.tasks.getByName(WarPlugin.WAR_TASK_NAME) as War
def jpiExtension = project.extensions.getByType(JpiExtension)

// src/main/webApp
def warconv = project.convention.getPlugin(WarPluginConvention)
mainAttributes.putValue('Resource-Path', warconv.webAppDir.absolutePath)
mainAttributes.putValue('Resource-Path', project.file(JpiPlugin.WEB_APP_DIR).absolutePath)

// add resource directories directly so that we can pick up the source, then add all the jars and class path
Set<File> libraries = conv.mainSourceTree().resources.srcDirs + war.classpath.files
Set<File> libraries = jpiExtension.mainSourceTree().output.files
mainAttributes.putValue('Libraries', libraries.findAll { it.exists() }.join(','))
}
}
Loading

0 comments on commit df6621b

Please sign in to comment.