Chapter 2
Please.build Build Language and Syntax
Venture into the expressive power and intricacies of the Please.build build language-a distinctive DSL meticulously crafted for maintainability, extensibility, and scalability. In this chapter, you'll not only uncover the mechanics of defining robust build specifications but also learn how to leverage custom rules, dynamic logic, and best practices. This journey unveils the language's unique features that elevate productivity and precision in even the most complex engineering workflows.
2.1 Anatomy of BUILD Files
At the heart of Please.build's build system lies the BUILD file, a domain-specific configuration script that meticulously defines build targets, dependencies, and associated actions. Understanding the anatomy of these BUILD files is essential, as they serve as the formal specification layer that composes deterministic and scalable build graphs. Their syntax, organization, and parsing semantics are carefully engineered to balance expressiveness with computational efficiency.
Underlying Syntax and Lexical Structure
BUILD files utilize a Python-inspired syntax, deliberately simplified to reduce ambiguities and parsing overhead while preserving flexibility for complex build rules. The syntax primarily consists of function calls that correspond to build rule invocations, argument lists composed of literals, lists, and nested dictionaries, and comments.
Tokens encompass identifiers, string literals (enclosed in single or double quotes), numeric literals, booleans, lists (denoted by square brackets), and dictionaries (delimited by curly braces). Comments begin with a # and extend to the end of the line.
The grammar refrains from supporting arbitrary control flow constructs such as loops or conditional statements directly to maintain build determinism. Instead, it provides carefully controlled conditionals through built-in expressions evaluated at parse time.
Structure and Order of Statements
A typical BUILD file consists of zero or more rule invocations, often interspersed with simple variable assignments or imports of other BUILD fragments. Rule invocations are the core statements, each mapping to a declarative description of a build target.
The order of statements within a BUILD file is, in general, non-significant semantically; however, variables used as arguments must precede their usage. Circular dependencies are prohibited, and thus the parser and evaluator enforce strict ordering constraints to ensure all references are resolvable.
A canonical build rule invocation has the structure:
rule_name( name = "target_name", srcs = ["file1.ext", "file2.ext"], deps = [":lib_a", "//pkg:lib_b"], visibility = ["//visibility:public"], ... ) where rule_name refers to a predefined or user-defined build rule type, and the arguments specify attributes for that target.
Context-Sensitive Constructs
While the core syntax is deterministic and declarative, BUILD files occasionally embed context-sensitive constructs, particularly in the form of conditional attribute assignments or platform-specific variants. These occur through conditional expressions or platform selectors that resolve to appropriate values depending on environment parameters.
For example, platform selectors are implemented as special dictionary mappings with keys representing target platforms:
deps = select({ "//platform:linux": [":linux_dep"], "//platform:windows": [":windows_dep"], "//conditions:default": [":default_dep"], }) The select() function serves as a context-sensitive dispatcher during the parse phase, selecting dependencies based on the build configuration. The underlying parser treats select as a first-class construct, integrating its evaluation into the build graph construction.
Parsing Lifecycle and Semantic Transformation
Parsing BUILD files is a multi-stage process that begins with lexical analysis, followed by syntactic parsing, semantic evaluation, and finally incorporation into the global dependency graph.
The lexer tokenizes the source into identifiable symbols while discarding whitespace and comments. The parser subsequently builds an abstract syntax tree (AST) composed of function call nodes, literals, and expressions based on the simplified grammar.
Semantic evaluation proceeds by resolving all symbols, validating attribute types, and expanding constructs like select(). The build system maintains a symbol table scoped per BUILD file to track variables and target definitions, enabling cross-referencing for dependencies.
Notably, the parser's deterministic design enforces immutability for all target attributes once parsed, ensuring build stability and cache correctness. Any syntactic or semantic error halts parsing and reports a precise diagnostic, facilitating rapid troubleshooting.
After successful parsing, the targets defined are integrated into the directed acyclic graph (DAG) representing the build. This graph links targets with their dependencies and defines the evaluation order required for incremental builds. The BUILD file thus acts as the foundational specification, encoding the blueprint for scalable and reproducible builds.
Annotated Example
Consider the following annotated BUILD fragment defining a simple library target:
library( ...