jenkinsci / gradle-jpi-plugin

Build Jenkins Plugins with Gradle
78 stars 50 forks source link

Configuring JENKINS_HOME #173

Closed wheelerlaw closed 3 years ago

wheelerlaw commented 3 years ago

I am trying to configure the JENKINS_HOME environment variable so that the Jenkins instance run using the server task will execute the groovy files in $JENKINS_HOME/init.groovy.d/. I tried setting the jenkinsHome value to a location (since that property looks like it configures the environment variable here):

tasks.named("server").configure {
    jenkinsHome = sourceSets.main.output.resourcesDir
}

However, I keep getting this error:

A problem occurred configuring project ':plugins:dependencies'.
> Could not create task ':plugins:dependencies:server'.
   > Cannot set the value of read-only property 'jenkinsHome' for task ':plugins:dependencies:server' of type org.jenkinsci.gradle.plugins.jpi.server.JenkinsServerTask.

How should I set this?

sghill commented 3 years ago

I prefer to solve this problem by introducing a new source set and sync task. It has great IDE support, makes it easy to enable groovyc features (like static compilation), and keeps the tasks focused and cacheable. Here's how to do it:

plugins {
  id 'groovy'
  id 'org.jenkins-ci.jpi' version '0.40.0'
}

def ourJenkinsVersion = '2.263.1'

jenkinsPlugin {
  jenkinsVersion = ourJenkinsVersion
}

repositories {
  jcenter()
  maven {
    name 'Jenkins'
    url 'https://repo.jenkins-ci.org/releases'
  }
}

// this enables many things, but these are the most important:
//    src/initd/groovy will now be the place to put your init scripts
//    initdImplementation will be the configuration to put your dependencies
sourceSets {
  initd
}

dependencies {
  // I prefer to bring in the official bom here as a platform, so we can use the same dependency versions Jenkins will use
  initdImplementation platform("org.jenkins-ci.main:jenkins-bom:$ourJenkinsVersion")
  initdImplementation 'org.codehaus.groovy:groovy-all' // version comes from the bom
  initdImplementation "org.jenkins-ci.main:jenkins-core:$ourJenkinsVersion" // needed to auto-complete core classes
}

// sync our init scripts into the existing `JENKINS_HOME`/init.groovy.d
def syncInit = tasks.register('syncServerInitScripts', Sync) {
  it.from sourceSets.initd.groovy
  it.into new File(jenkinsPlugin.workDir, 'init.groovy.d')
}

// ensure sync runs whenever we start up the server
tasks.named('server').configure {
  it.dependsOn syncInit
}

I tested this out by running a couple builds with a file in src/initd/groovy/startup.groovy.

Here it is with an update to the file:

$ ./gradlew server -i
...
> Task :syncServerInitScripts
Caching disabled for task ':syncServerInitScripts' because:
  Build cache is disabled
Task ':syncServerInitScripts' is not up-to-date because:
  Input property 'rootSpec$1' file /tmp/init-example/src/initd/groovy/startup.groovy has changed.
:syncServerInitScripts (Thread[Execution worker for ':',5,main]) completed. Took 0.008 secs.
:server (Thread[Execution worker for ':',5,main]) started.

Here it is with the file unchanged:

$ ./gradlew server -i
...
> Task :syncServerInitScripts UP-TO-DATE
Caching disabled for task ':syncServerInitScripts' because:
  Build cache is disabled
Skipping task ':syncServerInitScripts' as it is up-to-date.
:syncServerInitScripts (Thread[Execution worker for ':',5,main]) completed. Took 0.001 secs.
:server (Thread[Execution worker for ':',5,main]) started.

I wouldn't change JENKINS_HOME to be the main output's resources dir, because it would conflate a couple different things. There is a lot going on in the home directory that would cause task caching to always be considered out-of-date.

That being said, the simplest way to change the value of JENKINS_HOME is to update the value of jenkinsPlugin.workDir. The JpiPlugin class wires workDir to jenkinsHome as a read-only field. The field is set to the system property on process launch.

wheelerlaw commented 3 years ago

This is fantastic. Thank you for the assistance.

wheelerlaw commented 3 years ago

Do you think you could elaborate a little bit on the code in this plugin that pulls the .jar from the .hpi files? I have a parent project that doesn't have this plugin applied (instead it uses the jenkins-pipeline-shared-libraries-gradle-plugin for the shared library development), and I would like to just include a little bit of code in the build.gradle file that the same thing that this plugin does to extra the .jar from the .hpi file of a plugin dependency added to my build.gradle.

Edit: Actually, it might not be code in this plugin that is doing it, because when I add @jar after the dependency version so

initdImplementation 'org.jenkins-ci.plugins:cachet-gating:1.0.6'

becomes

initdImplementation 'org.jenkins-ci.plugins:cachet-gating:1.0.6@jar'

The plugin shows up as a jar dependency instead of an hpi dependency.

sghill commented 3 years ago

Do you think you could elaborate a little bit on the code in this plugin that pulls the .jar from the .hpi files?

This is done through variants with the JpiVariantRule. It's implemented as a ComponentMetadataRule. Basically for dependencies with POMs that have a packaging type of jpi or hpi, new compile and runtime variants are added for the jar.

The simplest way to apply this is to all components:

plugins {
  // the rule must be on the classpath, but the plugin isn't applied
  id 'org.jenkins-ci.jpi' version '0.40.0' apply false
}

dependencies {
  components.all(org.jenkinsci.gradle.plugins.jpi.JpiVariantRule)
  initdImplementation 'org.jenkins-ci.plugins:cachet-gating:1.0.6'
}

You can also apply it to just select modules using withModule if you'd prefer.

when I add @jar after the dependency

One reason to not use this syntax is it will force resolve the jar, but it won't run dependency resolution and get any of its transitives. Sometimes that's not a big deal, but for plugins that bring in other libraries it can be much more convenient to use the rule.

wheelerlaw commented 3 years ago

Okay, so I have another little riddle that I have been working on.

Basically I have a couple of local .hpi files in my repository for some custom Jenkins plugins. These plugins are loaded by a build script when we create our Jenkins container image. I also want the plugin to be added to the classpath so the IDE autocompletion works. The @jar syntax works fine for this because the transitive dependencies don't need to be resolved for just IDE autocomplation. However, it looks like the @jar syntax only works for remote .hpi files located in maven central or the likes. I have tried adding that JpiVariantRule stuff to my build.gradle, and have tried a couple of things, but no dice so far. Would you happen to know how to do a similar thing to crack open the local .hpi file and grab the right jar?

sghill commented 3 years ago

Basically I have a couple of local .hpi files in my repository for some custom Jenkins plugins.

One thing to consider upfront is if you can publish these plugins to a maven repo somewhere and resolve them from there with the JpiVariantRule. They'd behave like other plugins and be easier to maintain going forward.

This is what I'm picturing from the description:

.
├── build.gradle
├── libs
│   └── custom.hpi
├── settings.gradle
└── src
    └── main
        └── java
            └── org
                └── example
                    └── MyPlugin.java

it looks like the @jar syntax only works for remote .hpi files located in maven central or the likes.

It's working with remote things because there is also a .jar published alongside the .hpi (example).

Would you happen to know how to do a similar thing to crack open the local .hpi file and grab the right jar?

To get this working there will need to be a jar available and it will need to be added to the configuration.

If you're also building the custom plugins, it's probably simplest to also bring in the jar. If you only have the .hpi files, you could unpack libs/custom.hpi and copy <unpacked-dir>/WEB-INF/lib/custom-<version>.jar to libs/custom.hpi. The downside of this is it isn't obvious that you need to do this on each upgrade of custom.hpi.

Another option to consider is a custom task to unpack the .hpi to somewhere in the build directory, and add those files to the configuration. See Unpacking a subset of a ZIP file for this approach.