What is the rationale behind Jenkins's `load` vs. `library` functions?

303 Views Asked by At

This question follows from How can I dynamically load shared libraries in a Jenkinsfile and execute their same-named global variable methods?

Why does load return an object that scopes the loaded Groovy script's global variable methods whereas library does not?

I would like to understand the design/intent of load vs. library.
The linked documentation hasn't helped: it explains how to use the functions, but doesn't go into the rationale for why the functions behave as they do.
It seems like the only relevant difference between the two functions is where the Groovy scripts are loaded from: the local filesystem vs. a separate source control repo. It seems like a crippling deficiency of library to load everything into the global scope while not allow overriding previously-loaded content with subsequent calls to library.

As an example of what I'm asking: in the code below, I want to implement a "plugin"-style mechanism by which the Jenkinsfile loads Groovy scripts, each of which implements a function with the same name/signature.
This is achievable with load because the functions loaded by load are scoped to the return-value of load.
This does not seem to be achievable with library: I don't know what library returns, but unlike load, it's not an object on which I can invoke the shared library's global variable methods. Moreover, the first invocation of library loads the shared library's global variable methods into the filename's scope, and subsequent invocations do not override what was loaded by the first invocation.


Shared-library foo is laid out like this:

.
└── vars
    └── func.groovy
// foo shared-lib
// vars/func.groovy

def func(d) {
  println("shared-lib foo")
}

return this

Shared-library bar is laid out like this (it is the same layout as foo):

.
└── vars
    └── func.groovy
// bar shared-lib
// vars/func.groovy

def func(d) {
  println("shared-lib bar")
}

return this

The Jenkinsfile repo is laid out like this:

.
├── bar.groovy
├── foo.groovy
└── jenkinsfile
// foo.groovy

def func(d) {
  println("foo.groovy")
}

return this
// bar.groovy

def func(d) {
  println("bar.groovy")
}

return this
pipeline {
  agent any

  stages {
    stage('1') {
      steps {
        testLoad()
        testLibrary()
      }
    }
  }
}

def testLoad() {
  def lib_names = ['foo', 'bar']

  for (String lib_name in lib_names) {
    stage(lib_name) {
      def lib_file = lib_name + '.groovy'

      // The functions loaded by `load` are scoped to
      // the object `lib` -- super useful!
      def lib = load lib_file

      lib.func(42);
    }
  }
}

def testLibrary() {
  def repo_prefix = "ssh://[email protected]:8999/prj/"
  def repo_suffix = ".git"
  def branch = "@dev"
  def cred_id = 'ssh_credz'
  def lib_names = ['foo', 'bar']

  for (String lib_name in lib_names) {
    // The functions loaded by `library` are not scoped to
    // the object `lib` >:(
    def lib = library(identifier: lib_name + branch, retriever: modernSCM(
        [$class: 'GitSCMSource',
         remote: repo_prefix + lib_name + repo_suffix,
         credentialsId: cred_id,
         ]))

    // lib.func(42) // java.lang.ClassNotFoundException: null

    func.func(42) // Not useful because global variable `func` is set
                  // by the first call to `library` and not overridden
                  // by subsequent calls.
  }
}

The relevant output of this pipeline is:

foo.groovy
...
bar.groovy
...
Loading library foo@dev
...
shared-lib foo
...
Loading library bar@dev
...
shared-lib foo

Update: a comment from @daggett brought up the call() method. I tried implementing a call() function in the shared libraries, and the behavior is unchanged.
If shared-libraries foo and bar both have func.groovy and both implement def call(){...}, then yes, I can invoke func() as a shortcut for that shared library's primary function (instead of invoking func.func()), but still the problem described in the original post remains: if I do library foo@dev... then library bar@dev... then call func(), then foo's call() is invoked, rather than bar's.
I.e. I'd like to be able to load shared libraries (Groovy scripts) successively, and for the most recently-loaded one to "take effect" if it implements the same function as a previously-loaded one.
This is achievable with load, but so far appears to not be achievable with library.
If "it is what it is", I can accept that, but what I'm trying to learn, with this question, is the rationale behind this difference between load and library, because this seems a crippling limitation of library vs. load.

0

There are 0 best solutions below