From 8db6dfdf9f95e976a639b982fe01676eb67954aa Mon Sep 17 00:00:00 2001 From: Steve Hill Date: Sat, 16 May 2020 23:18:07 -0700 Subject: [PATCH] Introduce JenkinsServerTask - Adds jenkinsServerRuntimeOnly configuration - Launches server as JavaExec task - Allow JavaExecSpec to override server defaults - Docs for server behavior and customization - Backwards compatibility for debug properties passed through Gradle Fixes #151 --- README.md | 121 ++++++++++++++---- .../gradle/plugins/jpi/JpiExtension.groovy | 1 + .../gradle/plugins/jpi/JpiPlugin.groovy | 26 +++- .../gradle/plugins/jpi/ServerTask.groovy | 1 + .../jpi/server/JenkinsServerTask.groovy | 98 ++++++++++++++ .../JenkinsServerTaskSpec.groovy} | 16 +-- 6 files changed, 229 insertions(+), 34 deletions(-) create mode 100644 src/main/groovy/org/jenkinsci/gradle/plugins/jpi/server/JenkinsServerTask.groovy rename src/test/groovy/org/jenkinsci/gradle/plugins/jpi/{ServerTaskSpec.groovy => server/JenkinsServerTaskSpec.groovy} (78%) diff --git a/README.md b/README.md index e0fea373..5754df32 100644 --- a/README.md +++ b/README.md @@ -143,12 +143,104 @@ Examples: local Maven repository. * `gradle publish` - Deploy your plugin to the Jenkins Maven repository to be included in the Update Center. -* `gradle server` - Start a local instance of Jenkins (http://localhost:8080) with the plugin pre-installed for testing - and debugging. The HTTP port can be changed with the `jenkins.httpPort` project or system property, e.g. - `gradle server -Djenkins.httpPort=8082`. Since the server runs in the Gradle process, [increasing the memory available - to Gradle][configure-jvm] also increases the memory available to Jenkins. +* `gradle server` - Start a local instance of Jenkins with the plugin pre-installed for testing + and debugging. -[configure-jvm]: https://docs.gradle.org/current/userguide/build_environment.html#sec:configuring_jvm_memory +### Running Jenkins Locally + +The `server` task creates a [hpl][hpl], installs plugins, and starts up Jenkins on port 8080. The server runs +in the foreground. + +Plugins added to any of these configurations will be installed to `${jenkinsPlugin.workDir}/plugins`: +- `api` +- `implementation` +- `runtimeOnly` +- `jenkinsServer` + +#### Default System Properties + +Jenkins starts up with these system properties set to `true`: +- `stapler.trace` +- `stapler.jelly.noCache` +- `debug.YUI` +- `hudson.Main.development` + +Each can be overridden as described in the _Customizing Further_ section below. + +[hpl]: https://wiki.jenkins.io/display/JENKINS/Plugin+Structure#PluginStructure-DebugPluginLayout:.hpl + + +#### Customizing Port + +Jenkins starts by default on port 8080. This can be changed with the `--port` flag or `port` property. + +For example to run on port 7000: + +``` +$ ./gradlew server --port=7000 +``` + +or in build.gradle: + +```groovy +tasks.named('server').configure { + it.port.set(7000) +} +``` + +#### Customizing Further + +The `server` task accepts a [`JavaExecSpec`][javaexecspec] that allows extensive customization. + +Here's an example with common options: + +```groovy +tasks.named('server').configure { + execSpec { + systemProperty 'some.property', 'true' + environment 'SOME_ENV_VAR', 'HelloWorld' + maxHeapSize = '2g' + } +} +``` + + +#### Debugging + +To start Jenkins in a suspended state with a debug port of 5005, add the `--debug-jvm` flag: + +``` +$ ./gradlew server --debug-jvm + +> Task :server +Listening for transport dt_socket at address: 5005 +``` + +Debug options can be customized by the `server` task's `execSpec` action: + +```groovy +tasks.named('server').configure { + execSpec { + debugOptions { + port.set(6000) + suspend.set(false) + } + } +} +``` +``` +$ ./gradlew server --debug-jvm + +> Task :server +Listening for transport dt_socket at address: 6000 +``` + + + +#### Additional Server Dependencies + +Any additional dependencies for the `server` task's classpath can be added to the `jenkinsServerRuntimeOnly` +configuration. This can be useful for alternative logging implementations. ## Disabling SHA256 and SHA512 checksums when releasing a plugin @@ -174,23 +266,6 @@ If you combine java and groovy code and both provide extensions you need to eith - Use joint compilation, i.e. put your java source files into the groovy source path (src/main/groovy) - or force Gradle to use the old layout by including something like `sourceSets.main.output.classesDir = new File(buildDir, "classes/main")` in your build.gradle as a workaround. -## Debugging - -It is possible to attach a remote debugger to the Jenkins instance started by `gradle server`. - - $ ./gradlew server -Dorg.gradle.debug=true - -This command will run the gradle JVM with the appropriate options on the default debug port. -If more control is required, the JVM options can be set in more detail: - - $ ./gradlew server -Dorg.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 - -The `server` task enables several debug options: `stapler.trace`, `stapler.jelly.noCache` and `debug.YUI`. This -increases the page load time. All option can be changed and new options can be added by passing them as system -properties to the Gradle command line. - - $ ./gradlew -Dstapler.jelly.noCache=false server - ## Examples Here are some real world examples of Jenkins plugins using the Gradle JPI plugin: @@ -198,3 +273,5 @@ Here are some real world examples of Jenkins plugins using the Gradle JPI plugin * [Job DSL Plugin](https://github.com/jenkinsci/job-dsl-plugin) * [Selenium Axis Plugin](https://github.com/jenkinsci/selenium-axis-plugin) * [Doktor Plugin](https://github.com/jenkinsci/doktor-plugin) + +[javaexecspec]: https://docs.gradle.org/current/javadoc/org/gradle/process/JavaExecSpec.html diff --git a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiExtension.groovy b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiExtension.groovy index 6393dad8..8f4787a8 100644 --- a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiExtension.groovy +++ b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiExtension.groovy @@ -31,6 +31,7 @@ import org.gradle.util.ConfigureUtil */ class JpiExtension { final Project project + @Deprecated Map jenkinsWarCoordinates JpiExtension(Project project) { diff --git a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiPlugin.groovy b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiPlugin.groovy index e3db03cd..33ef7cab 100644 --- a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiPlugin.groovy +++ b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiPlugin.groovy @@ -19,6 +19,7 @@ import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.DependencySet import org.gradle.api.attributes.AttributeCompatibilityRule import org.gradle.api.attributes.AttributeDisambiguationRule import org.gradle.api.attributes.Bundling @@ -51,6 +52,7 @@ import org.gradle.util.GradleVersion import org.jenkinsci.gradle.plugins.jpi.server.GenerateJenkinsServerHplTask import org.jenkinsci.gradle.plugins.jpi.server.InstallJenkinsServerPluginsTask +import org.jenkinsci.gradle.plugins.jpi.server.JenkinsServerTask import static org.gradle.api.logging.LogLevel.INFO import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME @@ -115,10 +117,26 @@ class JpiPlugin implements Plugin { it.dependsOn(generateHpl) } - gradleProject.tasks.register(ServerTask.TASK_NAME, ServerTask) { - it.description = 'Run Jenkins in place with the plugin being developed' - it.group = BasePlugin.BUILD_GROUP // TODO - it.dependsOn(ext.mainSourceTree().runtimeClasspath, generateHpl, installPlugins) + def serverRuntime = gradleProject.configurations.create('jenkinsServerRuntimeOnly') { Configuration c -> + c.withDependencies { DependencySet deps -> + deps.add(gradleProject.dependencies.create("org.jenkins-ci.main:jenkins-war:${ext.coreVersion}@war")) + } + } + + gradleProject.tasks.register(JenkinsServerTask.TASK_NAME, JenkinsServerTask) { + it.description = 'Run Jenkins server locally with the plugin being developed' + it.group = 'Jenkins Server' + it.dependsOn(installPlugins) + it.jenkinsServerRuntime.set(serverRuntime) + it.jenkinsHome.set(ext.workDir) + def sysPropPort = System.getProperty('jenkins.httpPort') + if (sysPropPort) { + it.port.convention(sysPropPort) + } + def propPort = project.findProperty('jenkins.httpPort') as String + if (propPort) { + it.port.convention(propPort) + } } // set build directory for Jenkins test harness, JENKINS-26331 diff --git a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTask.groovy b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTask.groovy index 9a5f44f0..770bc149 100644 --- a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTask.groovy +++ b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTask.groovy @@ -26,6 +26,7 @@ import java.util.jar.JarFile * * @author Kohsuke Kawaguchi */ +@Deprecated class ServerTask extends DefaultTask { public static final String TASK_NAME = 'server' diff --git a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/server/JenkinsServerTask.groovy b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/server/JenkinsServerTask.groovy new file mode 100644 index 00000000..92adc8be --- /dev/null +++ b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/server/JenkinsServerTask.groovy @@ -0,0 +1,98 @@ +package org.jenkinsci.gradle.plugins.jpi.server + +import groovy.transform.CompileStatic +import org.gradle.api.Action +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.Configuration +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.options.Option +import org.gradle.process.JavaExecSpec + +import java.util.jar.JarFile + +@CompileStatic +class JenkinsServerTask extends DefaultTask { + public static final String TASK_NAME = 'server' + private static final Set DEFAULTED_PROPERTIES = [ + 'stapler.trace', + 'stapler.jelly.noCache', + 'debug.YUI', + 'hudson.Main.development', + ] as Set + private final List> execSpecActions = [] + + @Classpath + final Property jenkinsServerRuntime = project.objects.property(Configuration) + + @Input + final Provider jenkinsHome = project.objects.property(File) + + @Input + @Option(option = 'port', description = 'Port to start Jenkins on (default: 8080)') + final Property port = project.objects.property(String) + .convention('8080') + + @Input + @Option(option = 'debug-jvm', description = 'Start Jenkins suspended and listening on debug port (default: 5005)') + final Property debug = project.objects.property(Boolean) + .convention(false) + + @Internal + final Provider extractedMainClass = jenkinsServerRuntime.map { + def war = it.resolvedConfiguration + .firstLevelModuleDependencies + .find { it.moduleGroup == 'org.jenkins-ci.main' && it.moduleName == 'jenkins-war' } + .moduleArtifacts + .find { it.extension == 'war' } + .file + new JarFile(war).manifest.mainAttributes.getValue('Main-Class') + } + + JenkinsServerTask() { + doLast { + project.javaexec { JavaExecSpec s -> + s.classpath(jenkinsServerRuntime.get()) + s.mainClass.set(extractedMainClass) + s.args("--httpPort=${port.get()}") + s.systemProperty('JENKINS_HOME', jenkinsHome.get()) + for (String prop : DEFAULTED_PROPERTIES) { + s.systemProperty(prop, 'true') + } + passThroughForBackwardsCompatibility(s) + s.debug = debug.get() + execSpecActions.each { a -> a.execute(s) } + } + } + } + + void execSpec(Action action) { + execSpecActions.add(action) + } + + /** + * Discovers system properties set in gradle and passes them through to the task. + * + * Implemented for backwards-compatibility. Will be removed in 1.0.0. + * + * @param spec - to be mutated + */ + void passThroughForBackwardsCompatibility(JavaExecSpec spec) { + boolean anyDefined = false + for (String prop : DEFAULTED_PROPERTIES) { + def defined = System.getProperty(prop) + if (defined) { + anyDefined = true + logger.warn('Passing through system property {} to server is deprecated', prop) + spec.systemProperty(prop, defined) + } + } + if (anyDefined) { + logger.warn('Please configure server task with system properties directly') + logger.warn('Passing through will be removed in 1.0.0') + } + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTaskSpec.groovy b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/server/JenkinsServerTaskSpec.groovy similarity index 78% rename from src/test/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTaskSpec.groovy rename to src/test/groovy/org/jenkinsci/gradle/plugins/jpi/server/JenkinsServerTaskSpec.groovy index b226b558..c9dba690 100644 --- a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTaskSpec.groovy +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/server/JenkinsServerTaskSpec.groovy @@ -1,15 +1,16 @@ -package org.jenkinsci.gradle.plugins.jpi +package org.jenkinsci.gradle.plugins.jpi.server +import org.jenkinsci.gradle.plugins.jpi.IntegrationSpec import spock.lang.IgnoreIf import spock.lang.Unroll @IgnoreIf({ System.getProperty('os.name').toLowerCase().contains('windows') }) -class ServerTaskSpec extends IntegrationSpec { +class JenkinsServerTaskSpec extends IntegrationSpec { @Unroll def 'server task is working - Jenkins #jenkinsVersion'() { given: - projectDir.newFile('settings.gradle') << """\ + projectDir.newFile('settings.gradle') << """\ rootProject.name = "test-project" includeBuild('${path(new File(''))}') """ @@ -44,15 +45,14 @@ class ServerTaskSpec extends IntegrationSpec { } } - // run a separate process because the Jenkins shutdown kills the daemon - def gradleProcess = "${path(new File('gradlew'))} server -Djenkins.httpPort=8456 --no-daemon". - execute(null, projectDir.root) - def output = gradleProcess.text + def result = gradleRunner() + .withArguments('server', '--port=8456') + .build() + def output = result.output then: output.contains("/jenkins-war-${jenkinsVersion}.war") output.contains('webroot: System.getProperty("JENKINS_HOME")') - new File(projectDir.root, 'work/plugins/git.hpi').exists() where: jenkinsVersion | additionalPlugin