New model BinarySpec property is not readable

I defined a @Managed BinarySpec:

@Managed interface JuiceBinarySpec extends BinarySpec {
    List<String> getArgs()
    void setArgs(List<String> args)
}

When I generate the binary tasks, I want to pass the BinarySpec property to the task, but Gradle says the args element is not mutable. Here is the relevant code:

@BinaryTasks
void generateTasks(ModelMap<Task> tasks, final JuiceBinarySpec binary) {
    tasks.create("${binary.name}Juicer") { task ->
        task.args = binary.getArgs()
    }
}

Running this with Gradle 2.13, I get:

Cannot set value for model element ‘components.juiceComponent.binaries.juicer.args’ as this element is not mutable.

Can I use BinarySpec properties to set task properties? If so, how do I modify this code to make it work?

Full code here.

Yes, you can use a BinarySpec as input, but since Gradle is trying to ensure immutability for managed inputs, anything you do that could potentially modify the input will be blocked. In this case, I believe that since you’re able to modify the list returned by getArgs(), it’s considered illegal. You’ll have to make a copy of the list, or iterate and copy the elements, to avoid the error. That said, Java is not my main language, so I may be overlooking some other aspect of it here.

I fail to see how I can copy the args. According to the @Managed documentation, when I use Java 8, I should be able to use an interface default method and return a copy of the args:

@Managed
interface JuiceBinarySpec extends BinarySpec {
    default List<String> getArgs() {
        return new ArrayList<String>(args)
    }
    void setArgs(List<String> args)
}

But it gives me an error:

startup failed:
build file ‘/home/mdanjou/tmp/gradle-model-dsl-rule/01-simple/build.gradle’: 6: unexpected token: default @ line 6, column 5.
default List getArgs() {
^

Then I tried using an abstract class:

@Managed
abstract class JuiceBinarySpec implements BinarySpec {
    List<String> getArgs() {
        return new ArrayList<String>(args)
    }
    abstract void setArgs(List<String> args)
}

Which gives:

Property ‘args’ is not valid: it must have either only abstract accessor methods or only implemented accessor methods

And when I remove setArgs(), I can no longer set the args value in the model.

I use Java 8 and Gradle 2.13:

------------------------------------------------------------
Gradle 2.13
------------------------------------------------------------

Build time:   2016-04-25 04:10:10 UTC
Build number: none
Revision:     3b427b1481e46232107303c90be7b05079b05b1c

Groovy:       2.4.4
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015
JVM:          1.8.0_40 (Oracle Corporation 25.40-b25)
OS:           Linux 2.6.32-358.el6.x86_64 amd64

I am puzzled as how to make that work.

Looks like you’re beyond what I’ve figured out so far.

I didn’t have the list copy problem directly myself, but was using aManagedSet.withType(SomeType) { /* closure */ } which adds the closure to the collection for future adds, triggering the error. The solution was to instead iterate over the collection directly, as it’s supposed to be completely resolved anyway.

I made it work, but I don’t understand how or why just yet. I think it has to do with creation rules vs. mutation rules. At first I had:

model {
    components {
        juiceComponent(JuiceComponent) {
            binaries {
                juice(JuicerBinarySpec) {  // This is a creation rule
                    args = ['a', 'b']
                }
            }
        }
    }
}

And this code:

@BinaryTasks
void generateTasks(ModelMap<Task> tasks, final JuiceBinarySpec binary) {
    tasks.create("${binary.name}Juicer", JuicerTask) { task ->
        task.args = binary.getArgs()
    }
}

Would give me the not mutable exception (Cannot set value for model element ‘components.juiceComponent.binaries.juicer.args’ as this element is not mutable.).

But after I changed the creation rule to a mutation rule in the DSL like so:

model {
    components {
        juiceComponent(JuiceComponent) {
            binaries {
                juicer {  // This is now a mutation rule
                    args = ['a', 'b']
                }
            }
        }
    }
}

Then the exception went away. So now I wonder, how do I assign values to task inputs when a creation rule is used?

It took me a while to discover that the @BinaryTasks generateTask(...) method was being called TWICE. It is called a FIRST time for the binary defined internally:

@ComponentBinaries
void generateJuiceBinaries(ModelMap<JuiceBinarySpec> binaries, GeneralComponentSpec component) {
    binaries.create("builtinJuicer");
}

And it is being called a SECOND time for by the binary defined in the DSL:

model {
    components {
        juiceComponent(JuiceComponent) {
            binaries {
                source(JuiceBinarySpec) { // Called due to this line
...

So to avoid the non-mutable exception, the generateTask(...) method has to skip over the internal binary:

@BinaryTasks
void generateTasks(ModelMap<Task> tasks, final JuiceBinarySpec binary) {
    if (binary.name == "builtinJuicer") return
    tasks.create("${binary.name}Juicer", Juicer) { task ->
        task.args = binary.getArgs()
    }
}

It still feels strange to have an exception thrown when a non-mutable is only being read (reading it should not change it).