bytes.zone

moving genrules to library rules

Brian Hicks, August 14, 2023

In buck2 basics, we made two rules:

genrule(
  name = "some-target",
  out = "target-file",
  cmd = "echo 'Hello, World!' > $OUT",
)

genrule(
  name = "yelling-target",
  out = "target-file",
  cmd = "tr '[:lower:]' '[:upper:]' < $(location :some-target) > $OUT",
)

Let's lift these to named rules so that we have a nicer API and don't mix the commands with the parameters. Let's make a couple rules to say and yell (to implement some-target and yelling-target, respectively.) Open a new file talking.bzl and put this in it:

def _say_impl(ctx: "context"):
    return [DefaultInfo()]

say = rule(
    impl = _say_impl,
    attrs = {
        "message": attrs.string(),
        "out": attrs.string(default = "out"),
    },
)

This won't do anything yet, but it's about the minimum possible rule that lets us do this back in BUCK. I've rendered this as a diff so you can see the before and after:

+load(":talking.bzl", "say")

-genrule(
-  name = "some-target",
-  out = "target-file",
-  cmd = "echo 'Hello, World!' > $OUT",
-)
+say(
+  name = "some-target",
+  message = "Hello, World!",
+)

 genrule(
   name = "yelling-target",
   out = "target-file",
   cmd = "tr '[:lower:]' '[:upper:]' < $(location :some-target) > $OUT",
 )

Now we have :some-target redefined as a call to say, so let's implement it:

 def _say_impl(ctx: "context"):
+    out = ctx.actions.declare_output(ctx.attrs.out)
+
+    ctx.actions.write(out, ctx.attrs.message)
+
-    return [DefaultInfo()]
+    return [DefaultInfo(default_output = out)]

 say = rule(
     impl = _say_impl,
     attrs = {
         "message": attrs.string(),
         "out": attrs.string(default = "out"),
     },
 )

OK! Here's what's happening there:

At this point, if you run buck2 build //:yelling-target it'll produce the same output. We changed how we're building the file, but the build graph as a whole produces the same thing. I really like this property; it means I can improve parts of the build graph in isolation without worrying too much about upstream or downstream dependencies.

Anyway, let's do the same thing to :yelling-target now by making a yell rule:

def _yell_impl(ctx: "context"):
    out = ctx.actions.declare_output(ctx.attrs.out)

    ctx.actions.run(
        [
            "bash",
            "-c",
            "tr '[:lower:]' '[:upper:]' < $1 > $2",
            "--",
            ctx.attrs.src,
            out.as_output(),
        ],
        category = "yell",
    )

    return [DefaultInfo(default_output = out)]

yell = rule(
  impl = _yell_impl,
  attrs = {
      "src": attrs.source(),
      "out": attrs.string(default = "out"),
  },
)

We have some more stuff happening here:

Now in BUCK, we can modify our rules like so:

-load(":talking.bzl", "say")
+load(":talking.bzl", "say", "yell")

 say(
   name = "some-target",
   message = "Hello, World!",
 )

-genrule(
-  name = "yelling-target",
-  out = "target-file",
-  cmd = "tr '[:lower:]' '[:upper:]' < $(location :some-target) > $OUT",
-)
+yell(
+  name = "yelling-target",
+  src = ":some-target",
+)

Since src is a source, we provide the target pattern of the input file we want. That means our build graph is unchanged once again!

And we're done! Now we can call buck2 build //:yelling-target, same as before. Tada!

Anyway, that's all I have for Buck stuff—like I said in buck2 basics, I'll be watching the project to see when I can use it again!

If you'd like me to email you when I have a new post, sign up below and I'll do exactly that!

If you just have questions about this, or anything I write, please feel free to email me!