Chapter 66. Rule based model configuration

This chapter describes and documents what is essentially the foundation for the Gradle 3.0 and the next generation of Gradle builds. It is being incrementally developed during the Gradle 2.x stream and is in use for Gradle's support for building native binaries.

All of the mechanisms, DSL, API, and techniques discussed here are incubating (i.e. not considered stable and subject to change - see Appendix C, The Feature Lifecycle). Exposing new features early, during incubation, allows early testing and the incorporation of real world feedback ultimately resulting in a better Gradle.

The following build script is an example of a rule based build.

Example 66.1. an example of a simple rule based build

build.gradle

@Managed
interface Person {
  void setFirstName(String n); String getFirstName()
  void setLastName(String n); String getLastName()
}

class PersonRules extends RuleSource {
  @Model void person(Person p) {}

  @Mutate void setFirstName(Person p) {
    p.firstName = "John"
  }

 @Mutate void createHelloTask(CollectionBuilder<Task> tasks, Person p) {
    tasks.create("hello") {
      doLast {
        println "Hello $p.firstName $p.lastName!"
      }
    }
  }
}

apply plugin: PersonRules

model {
  person {
    lastName = "Smith"
  }
}

Note: The code for this example can be found at samples/modelRules/basicRuleSourcePlugin in the ‘-all’ distribution of Gradle.

Output of gradle hello

> gradle hello
:hello
Hello John Smith!

BUILD SUCCESSFUL

Total time: 1 secs

The rest of this chapter is dedicated to explaining what is going on in this build script, and why Gradle is moving in this direction.

66.1. Background

Gradle embraces domain modelling as core tenet. Focusing on the domain model as opposed to the execution model (like prior generation build tools such as Apache Ant) has many advantages. A strong domain model communicates the intent (i.e. the what) over the mechanics (i.e. the how). This allows humans to understand builds at a level that is meaningful to them.

As well as helping humans, a strong domain model also helps the dutiful machines. Plugins can more effectively collaborate around a strong domain model (e.g. plugins can say something about Java applications, such as providing conventions). Very importantly, by having a model of the what instead of the how Gradle can make intelligent choices on just how to do the how.

The move towards “Rule based model configuration” can be summarised as improving Gradle's ability to model richer domains in a more effective way. It also makes expressing the kinds of models present in today's Gradle more robust and simpler.

66.2. Motivations for change

Domain modelling in Gradle is not new. The Java plugin's SourceSet concept is an example of domain modelling, as is the modelling of NativeBinary in the Native plugin suite.

One distinguishing characteristic of Gradle compared to other build tools that also embrace modelling is that Gradle's model is open and collaborative. Gradle is fundamentally a tool for modelling software construction and then realizing the model, via tasks such as compilation etc.. Different domain plugins (e.g. Java, C++, Android) provide models that other plugins can collaborate with and build upon.

While Gradle has long employed sophisticated techniques when it comes to realizing the model (i.e. what we know as building things), the next generation of Gradle builds will employ some of the same techniques to building of the model itself. By defining build tasks as effectively a graph of dependent functions with explicit inputs and outputs, Gradle is able to order, cache, parallelize and apply other optimizations to the work. Using a “graph of tasks” for the production of software is a long established idea, and necessary given the complexity of software production. The task graph effectively defines the rules of execution the Gradle must follow. The term “Rule based model configuration” refers to applying the same concepts to building the model that builds the task graph.

Another key motivation is performance and scale. Aspects of the current approach that Gradle takes to modelling the build prevent pervasive parallelism and limit scalability. The new model is being designed with the requirements of modern software delivery in mind, where immediate responsiveness is critical for projects large and small.

66.3. Concepts

This section outlines the key concepts of rule based model configuration. Subsequent sections in this chapter will show the concepts in action.

66.3.1. The “model space”

The term “model space” is used to refer to the formal model, addressable by rules.

An analog with existing model is effectively the “project space”. The Project object is effectively the root of a graph of objects (e.g project.repositories, project.tasks etc.). A build script is effectively adding and configuring objects of this graph. For the most part, the “project space” is opaque to Gradle. It is an arbitrary graph of objects that Gradle only partially understands.

Each project also has its own model space, which is distinct from the project space. A key characteristic of the “model space” is that Gradle knows much more about it (which is knowledge that can be put to good use). The objects in the model space are “managed”, to a greater extent than objects in the project space. The origin, structure, state, collaborators and relationships of objects in the model space are first class constructs. This is effectively the characteristic that functionally distinguishes the model space from the project space: the objects of the model space are defined in ways that Gradle can understand them intimately, as opposed to an object that is the result of running relatively opaque code. A “rule” is effectively a building block of this definition.

The model space will eventually replace the project space, in so far as it will be the only “space”. However, during the transition the distinction is helpful.

66.3.2. Model paths

A model path identifies a path through a model space, to an element. A common representation is a period-delimited set of names. The model path "tasks" is the path to the element that is the task container. Assuming a task who's name is hello, the path "tasks.hello" is the path to this task.

TBD - more needed here.

66.3.3. Rules

The model space is defined in terms of “rules”. A rule is just a function (in the abstract sense) that either produces a model element, or acts upon a model element. Every rule has a single subject and zero or more inputs. Only the subject can be changed by a rule, while the inputs are effectively immutable.

Gradle guarantees that all inputs are fully “realized“ before the rule executes. The process of “realizing” a model element is effectively executing all the rules for which it is the subject, transitioning it to its final state. There is a strong analogy here to Gradle's task graph and task execution model. Just as tasks depend on each other and Gradle ensures that dependencies are satisfied before executing a task, rules effectively depend on each other (i.e. a rule depends on all rules who's subject is one of the inputs) and Gradle ensures that all dependencies are satisfied before executing the rule.

Model elements are very often defined in terms of other model elements. For example, a compile task's configuration can be defined in terms of the configuration of the source set that it is compiling. In this scenario, the compile task would be the subject of a rule and the source set an input. Such a rule could configure the task subject based on the source set input without concern for how it was configured, who it was configured by or when the configuration was specified.

There are several ways to declare rules, and in several forms. An explanation of the different forms and mechanisms along with concrete examples is forthcoming in this chapter.

66.3.4. Managed model elements

Currently, any kind of Java object can be part of the model space. However, there is a difference between “managed” and “unmanaged” objects.

A “managed” object is transparent and enforces immutability once realized. Being transparent means that its structure is understood by the rule infrastructure and as such each of its properties are also individual elements in the model space. Please see the Managed annotation for more information on creating managed model objects.

An “unmanaged” object is opaque to the the model space and does not enforce immutability. Over time, more mechanisms will be available for defining managed model elements culminating in all model elements being managed in some way.

66.3.5. References, binding and scopes

As previously mentioned, a rule has a subject and zero or more inputs. The rule's subject and inputs are declared as “references” and are “bound” to model elements before execution by Gradle. Each rule must effectively forward declare the subject and inputs as references. Precisely how this is done depends on the form of the rule. For example, the rules provided by a RuleSource declare references as method parameters.

A reference is either “by-path” or “by-type”.

A “by-type” reference identifies a particular model element by its type. For example, a reference to the TaskContainer effectively identifies the "tasks" element in the project model space. The model space is not exhaustively searched for candidates for by-type binding. The search space for a by-type binding is determined by the “scope” of the rule (discussed later).

A “by-path” reference identifies a particular model element by its path in model space. By-path references are always relative to the rule scope; there is currently no way to path “out” of the scope. All by-path references also have an associated type, but this does not influence what the reference binds to. The element identified by the path must however by type compatible with the reference, or a fatal “binding failure” will occur.

66.3.5.1. Binding scope

Rules are bound within a “scope”, which determines how references bind. Most rules are bound at the project scope (i.e. the root of the model graph for the project). However, rules can be scoped to a node within the graph. The CollectionBuilder.named() method is an example, of a mechanism for applying scoped rules. Rules declared in the build script using the model {} block, or via a RuleSource applied as a plugin use the root of the model space as the scope. This can be considered the default scope.

By-path references are always relative to the rule scope. When the scope is the root, this effectively allows binding to any element in the graph. When it is not, the children of the scope can be referred to by-path.

When binding by-type references, the following elements are considered:

  • The scope element itself.
  • The immediate children of the scope element.
  • The immediate children of the model space (i.e. project space) root.

For the common case, where the rule is effectively scoped to the root, only the immediate children of the root need to be considered.

66.4. Rule sources

One way to define rules, is via a RuleSource subclass. Such types can be applied in the same manner (to project objects) as Plugin implementations (i.e. via Project.apply()).

Example 66.2. applying a rule source plugin

build.gradle

@Managed
interface Person {
  void setFirstName(String n); String getFirstName()
  void setLastName(String n); String getLastName()
}

class PersonRules extends RuleSource {
  @Model void person(Person p) {}

  @Mutate void setFirstName(Person p) {
    p.firstName = "John"
  }

 @Mutate void createHelloTask(CollectionBuilder<Task> tasks, Person p) {
    tasks.create("hello") {
      doLast {
        println "Hello $p.firstName $p.lastName!"
      }
    }
  }
}

apply plugin: PersonRules

Rule source plugins can be packaged and distributed in the same manner as other types of plugins (see Chapter 58, Writing Custom Plugins).

The different methods of the rule source are discrete, independent rules. Their order, or the fact that they belong to the same class, are irrelevant.

Example 66.3. a model creation rule

build.gradle

@Model void person(Person p) {}

This rule declares the there is a model element at path "person" (defined by the method name), of type Person. This is the form of the Model type rule for Managed types. Here, the person object is the rule subject. The method could potentially have a body, that mutated the person instance. It could also potentially have more parameters, that would be the rule inputs.

Example 66.4. a model mutation rule

build.gradle

@Mutate void setFirstName(Person p) {
  p.firstName = "John"
}

This Mutate rule mutates the person object. The first parameter to the method is the subject. Here, a by-type reference is used as no Path annotation is present on the parameter. It could also potentially have more parameters, that would be the rule inputs.

Example 66.5. creating a task

build.gradle

@Mutate void createHelloTask(CollectionBuilder<Task> tasks, Person p) {
   tasks.create("hello") {
     doLast {
       println "Hello $p.firstName $p.lastName!"
     }
   }
 }

This Mutate rule effectively adds a task, by mutating the tasks collection. The subject here is the "tasks" node, which is available as a CollectionBuilder of Task. The lone input is our person element. As the person is being used as an input here, it will have been realised before executing this rule. That is, the task container effectively depends on the person element. If there are other configuration rules for the person element, potentially specified in a build script or other plugin, the will also be guaranteed to have been executed.

As Person is a Managed type in this example, any attempt to modify the person parameter in this method would result in an exception being thrown. Managed objects enforce immutability at the appropriate point in their lifecycle.

Please see the documentation for RuleSource for more information on constraints on how rule sources must be implemented and for more types of rules.

66.5. The “model DSL”

It is also possible to declare rules directly in the build script using the “model DSL”.

Example 66.6. the model dsl

build.gradle

model {
  person {
    lastName = "Smith"
  }
}

Continuing with the example so far of the model element "person" of type Person being present, the above DSL snippet effectively adds a mutation rule for the person that sets its lastName property.

The general form of the model DSL is:

model {
  «model-path-to-subject» {
    «imperative code»
  }
}
        

Where there may be multiple blocks.

It is also possible to create Managed type elements at the root level.

The general form of a creation rule is:

model {
  «element-name»(«element-type») {
    «imperative code»
  }
}
        

The following model rule is creating the person element:

Example 66.7. a DSL creation rule

build.gradle

person(Person) {
  firstName = "John"
}

Note: The code for this example can be found at samples/modelRules/modelDsl in the ‘-all’ distribution of Gradle.


The model DSL is currently quite limited. It is only possible to declare creation and general mutation rules. It is also only possible to refer to the subject by-path and it is not possible for the rule to have inputs. These are all limitations that will be addressed in future Gradle versions.

66.6. The model report

The built-in model task displays the model space as a tree. It can be used to see what the potential elements to bind to are.

Example 66.8. model task output

Output of gradle model

> gradle model
:model

------------------------------------------------------------
Root project
------------------------------------------------------------

model
    person
        firstName
        lastName

Currently the report only shows the structure of the model space. In future Gradle versions it will also display the types and values of the nodes. Future versions will also provide richer and more interactive ways of exploring the model space.

66.7. Limitations and future direction

Rule based model configuration is the future of Gradle. This area is fledgling, but under very active development. Early experiments have demonstrated that this approach is more efficient, able to provide richer diagnostics and authoring assistance and is more extensible. However, there are currently many limitations.

The majority of the development to date has been focused on proving the efficacy of the approach, and building the internal rule execution engine and model graph mechanics. The user facing aspects (e.g the DSL, rule source classes) are yet to be optimized for conciseness and general usability. Likewise, many necessary configuration patterns and constructs are not yet able to be expressed via the API.

In conjunction with the addition of better syntax, a richer toolkit of configuration constructs and generally more expressive power, more tooling will be added that will enable build engineers and users alike to comprehend, modify and extend builds in new ways.

Due to the inherent nature of the rule based approach, it is more efficient at constructing the build model than today's Gradle. However, in the future Gradle will also leverage the parallelism that this approach enables both at configuration and execution time. Moreover, due to increased transparency of the model Gradle will be able to further reduce build times by caching and pre-computing the build model. Beyond improved general build performance, this will greatly improve the experience when using Gradle from tools such as IDEs.

As this area of Gradle is under active development, it will be changing rapidly. Please be sure to consult the documentation of Gradle corresponding to the version you are using and to watch for changes announced in the release notes for future versions.