New model: how do I pass environment variables from BinarySpec to custom task

Using the new software model, I want to pass environment variables from a BinarySpec to a custom SourceTask extension. The best I could come up with for the DSL looks like this:

model {
    components {
        juiceComponent(JuiceComponent) {
            binaries {
                apple(JuiceBinarySpec) {
                    envVars {
                        var1(EnvVar) { value = 'value1'}
                        var2(EnvVar) { value = 'value2'}
                    }
                }
            }
            ...
        }
    }
}

However, in the code behind the scenes, I have some questions:

In the new model, is the BinarySpec really supposed to be the bridge between the DSL and the custom task @Input (and other inputs)?

How do I pass values from the BinarySpec to the custom task, I tried different way, and as an example, this code is my latest attempt and it fails:

class JuicerRules extends RuleSource {
    @BinaryTasks
    void generateTasks(ModelMap<Task> tasks, final JuiceBinarySpec binary) {
        binary.inputs.withType(FruitLanguage) { fruitLanguage ->
            tasks.create("${binary.name}Juicer", JuicerTask) { task ->
                // Cannot read the keys, so how to copy?
                println("EnvVars: "+binary.getEnvVars().keySet())
            }
        }
    }
}

When I run, I get this strange error:

Attempt to read from a write only view of model element ‘components.juiceComponent.binaries.apple.envVars’ of type ‘ModelMap’ given to rule ‘JuicerRules#generateTasks’

Code is online in a github project.

Passing a closure to the withType() method is a form of mutation. The closure is used to configure the item. Rule inputs are immutable, so any attempt to mutate them will result in an error. You’ll instead want to call withType() and then iterate over the result.

binary.inputs.withType(FruitLanguage).each { fruitLanguage ->
   ...
}

Yes. The idea being that tasks are implementation details. They simply do the work. Binaries are the domain model meant for configuration. You can directly configure tasks but ideally you expose this configuration via the model. Directly configuring tasks doesn’t scale well once you start talking multiple build variants and potentially several tasks for transforming each binary (compiling, linking, etc).

Iterating over the result gives me the same error (Attempt to read from a write only view of model element). The BinarySpec and its ModelMap property look like:

@Managed interface EnvVar {
    String getValue()
    void setValue(String value)
}
@Managed interface JuiceBinarySpec extends BinarySpec {
    ModelMap<EnvVar> getEnvVars()
}

It seems accessing the binary spec properties in this context, e.g. the size or anything else, is not allowed by Gradle:

@BinaryTasks
void generateTasks(ModelMap<Task> tasks, final JuiceBinarySpec binary) {
    binary.inputs.withType(FruitLanguage).each { fruitLanguage ->
        // Will throw "Attempt to read from a write only view of model element"
        println("EnvVars: "+binary.envVars.size())
        println("EnvVars: "+binary.envVars.keySet())
    }
}

The question is, how to copy envVars to the corresponding custom task input, since it can’t be read from the binary?

This may very well be a bug. Another thing that I find strange is that the rules are being eagerly evaluated even when running something like gradle help. @CedricChampeau can you provide any insight here?

Here is a variation on the problem. I have a List<String> args property that cannot be read from the binary, unless I set its value in the DSL model. Here are the relevant bits:

@BinaryTasks
void generateTasks(ModelMap<Task> tasks, final JuiceBinarySpec binary) {
    tasks.create("Juicer", DefaultTask) { task ->
        // Next line throws exception, unless the value is set in the model
        println(binary.args)
    }
}
...
model {
    components {
        juiceComponent(JuiceComponent) {
            binaries {
                orange(JuiceBinarySpec) {
                    // Uncomment out the next line to make the exception go away
                    //args = ['a']
                }
            }
        }
    }
}

Full code here.

I don’t know if this is the same problem as the one above with the ModelMap. I tried @Defaults in the hope that it would help but it did not. At this point I’d really welcome a workaround.