Buck: Adding Custom Rules to Buck
Support Ukraine. Help Provide Humanitarian Aid to Ukraine.

Adding Custom Rules to Buck

As of the writing of this document, the only official way to add rules to Buck is to fork the project and modify the source. We will, at some point, construct a beautiful and elegant extensions API. Until then, you'll want to have the documentation of our internal API handy, and then...

Start by editing KnownBuildRuleTypes. You'll need to edit createBuilder() and find the block where there are a large number of calls to builder.register().

builder.register(new YourDescription());
What is YourDescription? It's an implementation of the Description interface. This interface serves three roles in Buck:
  1. It provides the name for the rule type, eg. java_library
  2. It makes clear the parameters which are accepted by a rule
  3. It acts as a factory of BuildRule instances.

Defining Rule Parameters

This is done by implementing Description.createUnpopulatedConstructorArg(). This should return a java object, the public fields of which are the camelCased names of the parameters used by the rule in BUCK files. When referenced in BUCK files, the field name is snake_cased. That is "someParam" would become "some_param".

You may use primitives, enums, generic types, collections (including generic collections), and custom types for the fields of the constructor arg. If you are using a custom type which Buck does not know how to marshal from JSON, then you must implement a TypeCoercer. How to do this is beyond the scope of this doc.

Note: if a parameter might either be a local file or an output of a BuildRule, then you can use a SourcePathto represent that parameter.

If a parmater is considered to be optional, then the field should be declared using Guava's Optional class (eg. public Optional<String> name;)

Constructing new BuildRule instances

When Buck needs to construct a BuildRule it will call Description.createBuildRule(), passing in a populated constructor arg. Collection types on that arg will have been instantiated with an empty collection, even if they are declared as being optional.

The createBuildRule method is responsible for returning an instantiated BuildRule that can be used to construct whatever it is that we're adding this build rule for. The current instances typically inherit from AbstractBuildRule, and we suggest you do so too.

The created BuildRule will be registered with the BuildRuleResolver automatically. There are, however, plenty of cases where a BuildRule will want to depend on existing functionality that's already expressed as a BuildRule. In this case, it's fine to create that intermediate rule in the createBuildRule() method, add it to the resolver, and then add that newly created rule as a dependency of the one ultimately returned from the method. The only caveat: each rule must have its own, unique, BuildTarget, which is the name by which it's referred to in the action graph.

You are strongly encouraged to delay doing any work in your BuildRule until the getBuildSteps() method. In particular, don't do any work other than assigning fields in the constructor! The exception to this is working out the path to the output of your rule.

You can think of a Description as a factory of BuildRule instances. Similarly, you can think of a BuildRule as a factory of Steps that must be executed in order to create a specific output. Unlike rules, steps are executed sequentially in the order in which they are returned.

Ensuring Your Rule Is Rebuilt Correctly

If your rule extends AbstractBuildRule, then it's enough to simply annotate any field on that rule that contributes to its uniqueness with @AddToRuleKey. This is used by Buck to calculate the rule key, which, if the value changes between builds, will cause Buck to re-execute your BuildRule.

Another tip for ensuring your rule rebuilds correctly between builds is to ensure that the output directory is cleaned each time. Commonly, you'll see the first steps of a rule are something like:

Path outputDir = BuildTargets.getGenPath(target, "%s-output"); steps.add(new MakeCleanDirectoryStep(outputDir));
In your IDE, take a look at the key interfaces and classes to discover more about what you can do within Buck: