I want to understand a portion of code of my Jenkins's pipeline based on groovy DSL and closure.
I have a Jenkins file as follow:
foo {
var1 = "foo value 1"
var2 = "foo value 2"
}
I have a groovy script (foo.groovy in vars directory) in my Jenkins's shared lib:
def call(body) {
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
println config.var1 // display foo value 1 : for me the magic is here !!
}
I want to understand the groovy / jenkins mechanisme that when the closure is called the map config is set with the variables var1 and var2.
I understand (nearly) the closure mechanisme and the delegate method, but how can we know that the affectation of the config map to the delegate field of the closure allow the construction of the map with the variables declared in my Jenkinsfile ?
I hope I am quite clear in my question ! :)
Regards,
Stef
When a property is referenced within a closure, and that reference cannot be resolved within the closure, attempts are made to resolve it in various "places"
this
The closure's delegate property, which can be reassigned
The closure's owner
In your example, var1 and var2 are examples of references that cannot be resolved within the closure.
The following assigns the closure's delegate to config and ensures that this is the first "place" that will be used to resolve unresolved references
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
Therefore, when we set the properties var1 and var2 within the closure, they are resolved against config, i.e. set as key-value pairs of this Map.
If your example was changed to:
foo {
def var3 = "some value"
var1 = "foo value 1"
var2 = "foo value 2"
var3 = "some value"
}
var3 would not be resolved by config because it can be resolved within the closure.
Update
In response to your comment which (I think) is asking: why does setting the closure's delegate to a map cause a key-value pair to be added to that map?
When var1 = "foo value 1" can't be resolved within the closure, it is resolved instead against the map, because of this
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
so that effectively means we're calling
config.var1 = "foo value 1"
which is Groovy shorthand for
config.put("var1", "foo value 1")
Maybe it's a bit easier to understand if you change your code to call the put method directly, e.g.
def foo = {
put('var1', "foo value 1")
}
def call(body) {
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
println config.var1 // display foo value 1 : for me the magic is here !!
}
call(foo)
If you run this code in the Groovy Console, you'll see that it also prints "foo value 1".
If you're still struggling, maybe this question will help.
Related
In the context of Jenkins pipelines, I have some Groovy code that's enumerating a list, creating closures, and then using that value in the closure as a key to lookup another value in a map. This appears to be rife with some sort of anomaly or race condition almost every time.
This is a simplification of the code:
def tasks = [:]
for (platformName in platforms) {
// ...
tasks[platformName] = {
def componentUploadPath = componentUploadPaths[platformName]
echo "Uploading for platform [${platformName}] to [${componentUploadPath}]."
// ...
}
tasks.failFast = true
parallel(tasks)
platforms has two values. I will usually see two iterations and two tasks registered and the keys in tasks will be correct, but the echo statement inside the closure indicates that we're just running one of the platforms twice:
14:20:02 [platform2] Uploading for platform [platform1] to [some_path/platform1].
14:20:02 [platform1] Uploading for platform [platform1] to [some_path/platform1].
It's ridiculous.
What do I need to add or do differently?
It's the same issue as you'd see in Javascript.
When you generate the closures in a for loop, they are bound to a variable, not the value of the variable.
When the loop exits, and the closures are run, they will all be using the same value...that is -- the last value in the for loop before it exited
For example, you'd expect the following to print 1 2 3 4, but it doesn't
def closures = []
for (i in 1..4) {
closures << { -> println i }
}
closures.each { it() }
It prints 4 4 4 4
To fix this, you need to do one of two things... First, you could capture the value in a locally scoped variable, then close over this variable:
for (i in 1..4) {
def n = i
closures << { -> println n }
}
The second thing you could do is use groovy's each or collect as each time they are called, the variable is a different instance, so it works again:
(1..4).each { i ->
closures << { -> println i }
}
For your case, you can loop over platforms and collect into a map at the same time by using collectEntries:
def tasks = platforms.collectEntries { platformName ->
[
platformName,
{ ->
def componentUploadPath = componentUploadPaths[platformName]
echo "Uploading for platform [${platformName}] to [${componentUploadPath}]."
}
]
}
Hope this helps!
I want to assign a value to an existing variable, but the name of the variable is dynamic. How do I do that
def a1 = 0;
def b = 1;
eval("a${b} =1;");
print a1
No need for javascript here:
def name = 'someName';
def value = 'someValue';
new GroovyShell(this.binding).evaluate("${name} = '${value}'")
assert someName == value;
Though this doesn't answer your question exactly, an easy way around this is to drop your dynamic variables as map keys instead... avoid needing to eval them
def b = 1
def map = [:]
map."a${b}" = 1
assert map."a${b}" == 1
println(map) // result is [a1:1]
I would like to perform double substitution.
When printing:
def y = "\${x}"
def x = "world"
def z = "Hello ${y}"
println z
It prints:
Hello ${x}
When I would like it to print Hello World, I tried performing a double evaluation ${${}}, casting it to org.codehaus.groovy.runtime.GStringImpl, and a desperate ${y.toStrin()}
Edit:
To be more clear I mean this, but in Groovy:
https://unix.stackexchange.com/questions/68042/double-and-triple-substitution-in-bash-and-zsh
https://unix.stackexchange.com/questions/68035/foo-and-zsh
(Why I am doing this?: Because we have some text files that we need evaluate with groovy variables; the variables are many and in different part of the code are different, therefore I would like to have a solution working across all cases, not to have to bind each time each variable, not adding many lines of code)
So with what you have you're escaping the $ so it is just interpreted as a string.
For what you are looking to do I would look into Groovys's templating engines:
http://docs.groovy-lang.org/docs/next/html/documentation/template-engines.html
After reading your comment I played around with a few ideas and came up with this contrived answer, which is also probably not quite what you are looking for:
import groovy.lang.GroovyShell
class test{
String x = "world"
String y = "\${x}"
void function(){
GroovyShell shell = new GroovyShell();
Closure c = shell.evaluate("""{->"Hello $y"}""")
c.delegate = this
c.resolveStrategry = Closure.DELEGATE_FIRST
String z = c.call()
println z
}
}
new test().function()
But it was the closest thing I could come up with, and may lead you to something...
If I understand right, you are reading y from somewhere else. So you want to evaluate y as a GString after y and then x have been loaded. groovy.util.Eval will do this for simple cases. In this case, you have just one binding variable: x.
def y = '${x}'
def x = 'world'
def script = "Hello ${y}"
def z = Eval.me('x', x, '"' + script + '".toString()') // create a new GString expression from the string value of "script" and evaluate it to interpolate the value of "x"
println z
I'm trying to accept multiple values from a groovy method into a Jenkins pipeline and keep hitting Pipeline Workflow errors, any pointers as to what I'm doing wrong here is greatly appreciated.
(env.var1, env.var2, env.var3) = my_func()
def my_func(){
def a =10
def b =10
def c =10
return [a, b, c]
}
I get following error:
expecting ')', found ',' #(env.var1, env.var2, env.var3) = my_func()
You are using Groovy's multiple assignment feature incorrectly. It works when you assign a collection of values to a list of new variables. You can't use this type of assignment to assign values to an existing object. Your code also fails when executed in plain Groovy:
def env = [foo: 'bar']
(env.var1, env.var2, env.var3) = my_func()
println env
def my_func(){
def a =10
def b =10
def c =10
return [a,b,c]
}
Output:
1 compilation error:
expecting ')', found ',' at line: 3, column: 14
In Jenkins environment, env variable is represented not by a map, but by EnvActionImpl object which means it does not even support plus() or putAll() methods. It only overrides getProperty() and setProperty() methods, so you can access properties with env.name dot notation.
Solution
The simplest solution to your problem is to use multiple assignment correctly and then set env variables from variables. Consider following example:
node {
stage("A") {
def (var1, var2, var3) = my_func()
env.var1 = var1
env.var2 = var2
env.var3 = var3
}
stage("B") {
println env.var1
}
}
def my_func() {
def a = 10
def b = 10
def c = 10
return [a, b, c]
}
Keep in mind that var1, var2 and var3 variables cannot already exist in current scope, otherwise compiler will throw an exception.
I have a properties file which I call inside my Jenkins Pipeline Script to get multiple variables.
BuildCounter = n
BuildName1 = Name 1
BuildName2 = Name 2
...
Buildnamen = Name n
I call my properties file with: def props = readProperties file: Path
Now I want to create a loop to print all my BuildNames
for (i = 0; i < BuildJobCounterInt; i++){
tmp = 'BuildName' + i+1
println props.tmp
}
But of course this is not working. ne last println call is searching for a variable called tmp in my properties file. Is there a way to perform this or am I completely wrong?
EDIT:
This is my .properties file:
BuildJobCounter = 1
BuildName1 = 'Win32'
BuildPath1 = '_Build/MBE3_Win32'
BuildName2 = 'empty'
BuildPath2 = 'empty'
TestJobCounter = '0'
TestName1 = 'empty'
TestPath1 = 'empty'
TestName2 = 'empty'
TestPath2 = 'empty'
In my Jenkins pipeline I want to have the possibility to check the ammount of Build/TestJobs and automatically calle the Jobs (each BuildName and BuildPath is a Freestyle Job) To call all these Job I thought of calling the variables inside a for loop. So for every istep I have the Name/Path pair.
Try the below:
Change from:
println props.tmp
To:
println props[tmp]
or
println props."$tmp"
EDIT : based on OP comment
change from:
tmp = 'BuildName' + i+1
To:
def tmp = "BuildName${(i+1).toString()}"