Skip to content

Build Configuration

Grog takes a flexible approach to configuration that emphasizes simplicity and extensibility. It has minimal opinions about how you define builds, as long as each package can output a structured definition of its build targets.

You can configure Grog builds in four different ways:

  • Static configuration: Simple BUILD.{json,yaml} files that define build targets for each package. Great for getting started.
  • Makefiles: Add special comments to your existing Makefiles to run make goals while benefiting from Grog’s execution model. Perfect for gradually adopting Grog when you already use Makefiles.
  • Starlark: Use the Starlark language (similar to Python) to define builds with functions and macros. Familiar to Bazel users.
  • Pkl: Use the Pkl configuration language to create reusable configuration elements. Ideal for scaling your build configuration with strong typing.

The simplest way to define your build targets is to create a BUILD.json or BUILD.yaml file in your package directory. This file defines all build targets for that package in your monorepo.

targets:
- name: build_app
command: npm run build
dependencies:
- :generate_proto
- //path/to/other/package:target_name
inputs:
- src/**/*.js
- package.json
outputs:
- dist/bundle.js
- name: generate_proto
command: "protoc --js_out=src/generated proto/\*.proto"
inputs:
- proto/\*.proto
outputs:
- dir::src/generated

Save this as BUILD.yaml in your project directory.

You can also define simple aliases that point to other targets:

aliases:
- name: default
actual: :build_app

Running grog build :default would build :build_app.

For a complete list of available options, see the target configuration reference.

If you already use Makefiles extensively and want to continue using them while benefiting from Grog’s execution model, you can add special comments to your existing Makefiles.

The schema is straightforward:

  • Grog looks for a line that starts with # @grog
  • Everything between that line and the next make goal is parsed as YAML configuration
  • The command will be make <goal>
  • If the name is left empty, the goal name becomes the target name

Here’s an example:

# @grog
# inputs:
# - src/**/*.js
# - package.json
# outputs:
# - dist/bundle.js
# dependencies:
# - :generate_proto
# - //path/to/other/package:target_name
build_app:
npm run build
# @grog
# inputs:
# - proto/*.proto
# outputs:
# - dir::src/generated
generate_proto:
protoc --js_out=src/generated proto/*.proto

This Makefile exposes the targets build_app and generate_proto to Grog, allowing you to run them with grog build :build_app or grog build :generate_proto.

Starlark is a Python-like configuration language originally created for Bazel. If you’re familiar with Bazel or prefer a Python-like syntax, Starlark provides a powerful way to define your build targets using functions and macros.

Here’s the same build configuration as above, but using Starlark:

BUILD.star
target(
name = "build_app",
command = "npm run build",
dependencies = [
":generate_proto",
"//path/to/other/package:target_name",
],
inputs = [
"src/**/*.js",
"package.json",
],
outputs = ["dist/bundle.js"],
)
target(
name = "generate_proto",
command = "protoc --js_out=src/generated proto/*.proto",
inputs = ["proto/*.proto"],
outputs = ["dir::src/generated"],
)

Save this as BUILD.star or BUILD.bzl in your project directory.

One of Starlark’s key features is the ability to create reusable macros using functions. For example, if you want a standard setup for TypeScript projects that includes build, test, and format targets:

rules.star
def ts_package(name, srcs = [], deps = []):
"""Creates a TypeScript package with build, test, and format targets."""
srcs = ["./src/**"] + srcs
target(
name = name,
command = "tsc",
inputs = srcs,
outputs = ["./dist/**"],
dependencies = deps,
)
target(
name = name + "_test",
command = "node --test ./tests",
inputs = srcs + ["./tests/**"],
dependencies = deps,
)
target(
name = name + "_format",
command = "prettier --write .",
inputs = srcs + ["./tests/**"],
)

You can then use this macro in your BUILD files:

packages/example/BUILD.star
load("//rules.star", "ts_package")
ts_package(
name = "example",
deps = ["//packages/common"],
)

This will create three targets: example, example_test, and example_format.

The load() statement allows you to import macros and functions from other Starlark files:

  • Use //path/to/file.star for absolute paths from the workspace root
  • Use relative paths like ./rules.star or ../shared/rules.star for relative imports

As your codebase grows, maintaining consistent build configurations across packages becomes tedious. For example, if you want a generic Java build process that’s consistent across your entire monorepo, copying configurations between packages is error-prone and difficult to maintain.

Pkl is a configuration language designed specifically for creating reusable configuration macros. With Pkl, you can:

  • Define configuration macros that can be shared within your monorepo or even across the internet
  • Get semantic highlighting and auto-completion with IDE plugins
  • Create modular, reusable build configurations

Here’s the same build configuration as above, but using Pkl:

amends "package://grog.build/releases/v0.16.1/grog@0.16.1#/package.pkl"
targets {
new {
name = "build_app"
command = "npm run build"
dependencies {
":generate_proto"
"//path/to/other/package:target_name"
}
inputs {
"src/**/*.js"
"package.json"
}
outputs {
"dist/bundle.js"
}
}
new {
name = "generate_proto"
command = "protoc --js_out=src/generated proto/*.proto"
inputs {
"proto/*.proto"
}
outputs {
"dir::src/generated"
}
}
}

One of Pkl’s strengths is the ability to create reusable build macros. For example, if you want a standard setup for Next.js projects that includes installing dependencies and building the app, you can create a reusable macro:

import "package://grog.build/releases/v0.16.1/grog@0.16.1#/package.pkl"
// Create a function that returns
// a list of targets for a Next.js app
// using pkl's class-as-a-function pattern
class App {
app_name: String
fixed targets: Listing<package.Target> = new Listing<package.Target> {
new {
name = app_name + "_install"
command = "npm install"
inputs {
"package.json"
"package-lock.json"
}
}
new {
name = app_name + "_build"
command = "npm run build"
dependencies {
":" + app_name + "_install"
}
inputs {
"src/**/*.js"
"src/**/*.jsx"
"src/**/*.ts"
"src/**/*.tsx"
}
outputs {
"dir::.next"
}
}
}
}

You can store this in a file called nextjs.pkl somewhere in your repository and import it wherever you need it:

import "nextjs.pkl"
targets {
// This will add both the install and build targets
// with the prefix "my-app"
...(nextjs.App) {
app_name = "my-app"
}.targets
// You can also add custom targets
new {
name = "custom_target"
command = "echo 'Hello, world!'"
}
}

This approach allows you to standardize build configurations across your monorepo while still allowing for customization where needed.