Please (Polyglot)
A lightweight, Go-implemented incarnation of the Blaze/Bazel/Buck design, created at fintech firm Thought Machine: a cross-language build orchestrator whose BUILD files — written in a restricted Python dialect — turn a whole repository into one hash-keyed, sandboxed action graph that is parallelized, locally and remotely cached, and (since v17) extended entirely through downloadable language plugins.
| Field | Value |
|---|---|
| Language | Go (engine, ~86%) + Starlark/restricted-Python (the BUILD/.build_defs language) |
| License | Apache-2.0 |
| Repository | thought-machine/please |
| Documentation | please.build · Lexicon · Config reference |
| Category | Polyglot Build Orchestrator |
| Workspace model | Single repo rooted at a .plzconfig marker file; the whole tree is one workspace of BUILD-file packages |
| First released | Open-sourced by Thought Machine in late 2017 (born from "an impromptu discussion in a pub one Friday evening") |
| Latest release | v17.30.0 (April 21, 2026) |
Latest release:
v17.30.0, released April 21, 2026. Thev17line is significant: it completed the migration begun inv16of moving the bundled per-language rules (Go,Python,Java,C++, …) out of the engine and into separately-versioned plugins fetched as subrepos. As of June 5, 2026 a repo therefore declares both an engine version and an explicit set of language plugins in.plzconfig. Source citations below are againstmaster(thought-machine/please) and the official docs at please.build.
Overview
What it solves
Please targets the same problem as Bazel and Buck2: one repository containing many languages, built and tested as a single coherent graph, where a change to a shared .proto or library rebuilds exactly the downstream Go, Python, and Java that depend on it — no more, no less. It was written at Thought Machine because their codebase spanned "Javascript, Python, Java and Go" plus other technologies, and invoking a different native build tool per language (go build, mvn, pip, webpack) gave no cross-language dependency graph, no shared cache, and no single test command.
Where Please consciously diverges from Bazel is weight. From the project's own FAQ, the team "have slightly different goals, specifically we're aiming Please at being lighter weight" than Bazel, while keeping the parts they valued from Buck (which they used internally) — notably "test sandboxing" and "a stronger focus on BUILD language correctness." The engine is a single statically-linked Go binary (no JVM, no daemon required for small repos), bootstrapped per-repo by a committed pleasew wrapper script that downloads the pinned version on first run — so a checkout builds with the right plz without a global install.
The build model is the canonical content-addressed one: every target declares its exact srcs, deps, outs, and cmd; the engine hashes those inputs; and an action whose inputs are unchanged is never re-run — its outputs are pulled from a cache instead, possibly a cache shared by the whole org. Within the polyglot survey, Please is the "Bazel, but small and Go-native" data point; contrast the heavyweight reference Bazel and Buck2, the Python-rule-set Pants, and the schema-first Moon.
Design philosophy
The README's one-sentence positioning (thought-machine/please):
"Please is a cross-language build system with an emphasis on high performance, extensibility and reproducibility. It supports many popular languages and can automate nearly any aspect of your build process."
Two deliberate choices follow and shape the whole tool:
BUILDfiles are programs, not data. The build language is "a restricted subset of Python", chosen over XML/JSON because it gives "more power and (in our opinion) a significantly nicer format." ABUILDfile can loop, branch, and call shared functions (subincluded from.build_defsfiles) to generate targets programmatically — but the dialect is sandboxed (no arbitrary I/O, deterministic) so parsing stays a pure function and the graph stays cacheable.Go, for operational lightness. They choseGobecause it "avoided JVM startup overhead, Python's threading limitations, and C++'s complexity" — the binary starts instantly and parallelizes parsing/building across goroutines without a persistent server. This is the concrete lever behind the "lighter weight than Bazel" goal.
The same language powers built-in and user rules: every go_binary, python_test, etc. is itself a function written in the BUILD dialect (now shipped in plugins), so "built-in rules use the same language as user-defined targets" — there is no privileged native-rule tier as in early Bazel.
How it works
Repository, packages, and labels
A Please repository is rooted at the .plzconfig file: "The .plzconfig file marks the root of a Please repository." Any directory containing a BUILD file is a package — "analogous to Makefiles in that they define buildable targets for that directory." Targets are addressed by build labels:
//src/core:core # one target 'core' in package //src/core
//src/core:all # every target in that package
//src/... # every target in the package tree (recursive)
:core # local reference, same BUILD file
PUBLIC # visibility pseudo-label, ~ //...A minimal genrule — the universal escape hatch every other rule is built on — shows the declare-inputs/declare-outputs contract (build rules docs):
# BUILD
genrule(
name = "word_count",
srcs = ["file.txt"],
outs = ["file.wordcount"],
cmd = "wc $SRCS > $OUT",
)$SRCS, $OUT, $PKG, $NAME, $TMP_DIR, $TOOLS and friends are the only environment exposed; host env requires explicit pass_env. Cross-target references in cmd use substitutions — $(location //path:target), $(exe //path:target), $(hash //path:target) — so commands never hard-code paths into plz-out.
Sandboxing and hashing
Each rule runs "in an isolated temporary directory containing only its declared inputs"; with sandbox = True the action is additionally placed "within a separate network and process namespace" (Linux namespaces). This is what makes the input hash trustworthy: an action cannot read an undeclared file, so its output is a pure function of (rule definition hash, source hashes, dependency output hashes). Outputs land in plz-out/bin/ (binaries) or plz-out/gen/ (everything else), and only declared outs survive — anything else the command writes is discarded with the sandbox.
Subincludes and .build_defs
Reusable rule logic lives in .build_defs files and is pulled in with subinclude, the BUILD-language analogue of import. Per the lexicon, subinclude "includes the output of a build target as extra rules in this one" — the contents are merged into the calling module's globals, so the imported functions are then callable directly. [Parse] PreloadSubincludes in .plzconfig preloads a set of these before any BUILD file is parsed, giving a repo a shared prelude of macros.
Plugins and the v17 model
Since v17, language rule-sets are plugins fetched as subrepos. Bootstrapping a Go-aware repo:
plz init # writes .plzconfig + the pleasew wrapper script
plz init plugin go # adds the Go plugin as a subrepo + [Plugin "go"] config.plzconfig then pins the engine version and the plugins together:
# .plzconfig
[please]
version = 17.30.0
[plugin "go"]
target = //plugins:go
importpath = github.com/thought-machine/exampleBuilt-in rules thus version independently of the engine; a repo upgrades Go tooling rules without upgrading plz itself.
The five dimensions
1. Workspace declaration & topology
- Root marker, not a member list. A repo is rooted wherever
.plzconfiglives; the workspace is the entire tree, discovered by walking it forBUILDfiles. There is no explicitmembers/packagesarray (contrast Cargo's[workspace] members = [...]or pnpm'spnpm-workspace.yamlglobs). Topology is implicit: every directory with aBUILDfile is a package; every package's targets form the leaves of one global graph. - Discovery is configurable and prunable.
[Parse] BuildFileNamelets a repo renameBUILDto anything ("you could reconfigure them here to be something else");[Parse] BlacklistDirsexcludes directories from the recursive search (vendored trees, generated dirs). - Layered config = layered "workspaces."
.plzconfigis merged from up to six sources in priority order —/etc/please/plzconfig,~/.config/please/plzconfig,.plzconfig,.plzconfig_<arch>,--profilevariants, and.plzconfig.local(highest) — so machine, user, repo, arch, and per-developer overrides compose without editing the committed file. - Subrepos = nested/foreign workspaces.
http_archive,github_repo, and the plugin mechanism mount another repo (or a downloaded archive) as a subrepo, addressed with the triple-slash label syntax///third_party/go/...//pkg:target. This is the closest Please gets to a multi-root workspace: third-party code and language plugins are first-class sub-graphs you depend on like any local target.
2. Dependency handling & isolation
- No hoisting, no symlink store — vendored subrepos. Please has no concept of a hoisted
node_modules(npm) or a content-addressed virtual store (pnpm). Third-party deps are "a concept called 'subrepos' which allows fetching arbitrary dependencies and attaching build rules to them." Each foreign package becomes a target in a subrepo, isolated by the sandbox at build time. - Per-ecosystem fetch rules, all pinned. Language plugins provide
pip_library()(Python),maven_jar()(Java), andgo_module()/go_repo(Go). Each "requires explicit transitive dependency declarations to pin dependencies & guarantee reproducibility," and downloads are SHA256-verified (thehashesparameter) — a corrupted or changed artifact fails the build. There is no separate top-level lockfile format; the pin is theBUILDrule (revision = ...,hashes = [...]), versioned in git. - Local cross-references are just labels. A library in one package is depended on from another purely by its
//path:targetlabel — nopath=/workspace:protocol, no publishing step. The dependency graph is the cross-reference mechanism; topological build order falls out of it automatically. - License gating.
pip_library/maven_jarauto-detect licenses and check them against a configured allowlist — dependency policy enforced at fetch time.
3. Task orchestration & scheduling
One global action DAG. Every target is a node;
srcs/deps/toolsare edges.plz build //...resolves the transitive closure, then executes ready actions concurrently across a worker pool sized by[please] NumThreads(default CPU count + 2, override-n/--num_threads). Parsing is itself parallel (goroutines perBUILDfile).Change detection by input hash, not timestamps. An action is skipped when its computed input hash matches a cached result — "correctness in cache invalidation through dependency hashing rather than timestamps."
plz-outis reused for plain incremental builds even with caching off.First-class affected-target queries.
plz query changesis the CI/monorepo-slicing primitive. From the source (src/query/changes.go), it offers two entry points —DiffGraphs(compare two graphs, e.g. across a git revision) andChanges(a faster current-state-plus-file-list path). It maps a changed file to targets by walking up to the nearest package and finding targets that claim the file viaHasAbsoluteSource, compares rule + source hashes (targetChanged) to confirm a real change, then expands to reverse dependencies viaFindRevdeps. A load-bearing comment:"Note that this is not symmetric; targets that have been removed from 'before' do not appear (because this is designed to be fed into 'plz test' and we can't test targets that no longer exist)."
The canonical CI pattern is therefore
plz test $(plz query changes --since <ref>)— build/test only what a diff actually touched, transitively.plz watchre-runs targets on file change for an inner-loop equivalent of the affected-target slice.
4. Caching & remote execution
- Two-tier local + shared cache, all content-addressed. Caches layer beneath
plz-out. The directory cache ([Cache] Dir, default~/.cache/please) is the default, storing artifacts in a local tree that "allows extremely fast rebuilds when swapping between different versions of code (notably git branches)." Caveat from the docs: it "is not threadsafe or locked … so sharing the same directory between multiple projects is probably a Bad Idea." - HTTP cache for teams.
[Cache] HttpUrlpoints at "a simple API based on PUT and GET to store and retrieve opaque blobs" — backed by nginx+WebDAV or CI artifact services;HttpWriteablecontrols write-back (read-only by default for untrusted clients). - Command-driven cache (
RetrieveCommand/StoreCommand) shells out for S3-style backends, streaming tar via stdin/stdout withCACHE_KEYin the environment. Read-only is configurable;RetrieveCommandis mandatory. - Artifacts cache only on success — "artifacts are only stored in the cache after a successful build or test run."
- Remote execution via REAPI. Please speaks the Remote Execution API (
v2.1): "Please makes use of the Remote Execution API to distribute work. This is a generic gRPC-based API with a number of options for the server-side." Configured under[Remote](URL,Instance,NumExecutors,Secure), it works against any REAPI server —Buildbarn,BuildBuddy,NativeLink,BuildGrid— sharing the same content-addressable storage and action cache as Bazel. Theremote_filerule additionally needs the Remote Asset API. The docs are candid that remote execution is "still experimental" and "setting it up can be a reasonable amount of work."
5. CLI / UX ergonomics
- Verb + label, with rich wildcards. The command boundary is uniform:
plz build,plz test,plz run,plz cover,plz exec(hermetic-sandbox run),plz watch, all taking labels with:alland/...wildcards.plz runbuilds-and-executes;plz testemits xUnit XML. - Label-based language filtering.
-i/--includeand-e/--excludefilter the selected set by labels (e.g. language tagsgo,python,java,cc), with--excludetaking priority — soplz test //... -i goruns only theGotests in the whole tree. This is Please's analogue of Turborepo's--filter/pnpm's-F, but keyed on graph labels rather than package names. plz queryis the introspection surface. Subcommands cover the graph:deps/reverseDeps,changes(affected targets vs a revision/file set),somepath(path between two targets),input/output(transitive files),whatinputs/whatoutputs(file→target mapping),alltargets,graph(JSON),rules(machine-readable rule schemas),print, andfilter(apply--include/--exclude).- Repo-pinned via
pleasew. Becauseplz initcommits apleasewwrapper that downloads the version named in.plzconfig, every contributor and CI runner uses the same engine without a global install — a notable ergonomic win over tools that assume a system-wide binary. - Quality-of-life verbs.
plz fmt(buildifier-basedBUILDformatting),plz init/plz init plugin,plz update(self-update to the pinned version),plz op(re-run the last command),plz gc(find unused targets, experimental),-o/--overridefor ad-hoc config overrides, and--profilefor named config variants.
Strengths
- Bazel-class model at a fraction of the weight. One static
Gobinary, instant startup, no mandatory daemon — the content-addressed action graph, sandboxing, and REAPI remote execution without the JVM and operational heft of Bazel. - Programmable but deterministic
BUILDlanguage. A restrictedPythondialect loops/branches/factors-out viasubinclude, yet parses as a pure function so the graph stays cacheable — more expressive than declarative TOML/JSON manifests. - First-class affected-target slicing.
plz query changes --since <ref>feedingplz testis a built-in, principled CI optimization, computed from real rule and source hashes plus reverse-dependency expansion. - Self-bootstrapping repos. The committed
pleasewwrapper + version pin in.plzconfigmeans a clean checkout builds reproducibly with zero global setup. - Layered, composable config. Six-level
.plzconfigprecedence cleanly separates machine/user/repo/arch/profile/local overrides. - Uniform extension model. Built-in and user rules use the same language;
v17plugins version language rule-sets independently of the engine. - Pinned, hash-verified, license-gated third-party deps across
Go/Python/Javavia subrepos.
Weaknesses
- Smaller ecosystem and integration surface than Bazel/Buck2. Fewer pre-written rules, fewer integrations, a smaller community; remote execution is explicitly "still experimental" and fiddly to stand up.
- No package-manager interop layer. Unlike language-native tools (Cargo, uv, Go modules), adopting Please means writing
BUILDfiles and re-expressing third-party deps as subrepos — a real migration cost; there is noimport the existing manifestpath. - Whole-tree workspace, no explicit member curation. Topology is implicit in
BUILD-file placement; there is nomembersarray to scope or partition a repo into named sub-workspaces (onlyBlacklistDirsto prune). - Directory cache isn't safe to share across projects (not locked beyond the repo lock) — a footgun for multi-checkout setups.
- Restricted
Pythonis still a learning curve, and a programmable build language can hide complexity that pure-data manifests cannot. - Single-vendor stewardship. Primarily driven by Thought Machine; niche outside organizations that have adopted it.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
Single Go binary, no mandatory daemon (vs Bazel's JVM server) | Instant startup, easy distribution, "lighter weight" than Bazel | Less of a persistent-analysis-cache story than a long-lived server; cold parse each invocation |
Repo rooted at a .plzconfig marker; whole tree is one workspace | Zero ceremony — no members list to maintain; every BUILD dir just works | No explicit sub-workspace scoping; can't easily partition one repo into named member sets |
BUILD language = restricted Python (programs, not data) | Loops/macros/subinclude give power and "a significantly nicer format" than XML/JSON | A learning curve; programmable builds can hide complexity vs declarative manifests |
| Sandboxed actions + input hashing for cache keys | Trustworthy, timestamp-free invalidation; aggressive local + remote caching | Every input must be declared; undeclared-file reads break (intentionally), demanding precise srcs |
| Third-party deps as pinned, hash-verified subrepos | Reproducible, license-gated, polyglot dependency fetch with no global package state | No hoisting/virtual store; must re-express each ecosystem's deps as BUILD rules (migration cost) |
Language rules as versioned plugins (v17+) | Engine and rule-sets evolve independently; same language for built-in and user rules | More moving parts to pin; a repo now tracks engine version and a set of plugin versions |
REAPI (v2.1) for remote cache/execution | Reuses the Bazel-ecosystem CAS/action-cache servers (Buildbarn, BuildBuddy, NativeLink) | Remote execution is "still experimental" and "a reasonable amount of work" to configure |
plz query changes --since as a built-in affected-target slice | Principled CI optimization from rule+source hashes + reverse-dep expansion | Asymmetric (removed targets omitted); relies on accurate srcs and a clean graph for correctness |
Sources
- thought-machine/please — GitHub repository (README tagline, languages, license)
- please.build — official documentation
- Basics: BUILD files, packages, labels, commands
- Config reference —
.plzconfigsections and precedence - Caching — directory / HTTP / command-driven caches
- Remote execution — REAPI v2.1,
[Remote]config - How custom build rules work —
genrule, sandboxing, env vars - Third-party dependencies — subrepos,
pip_library/maven_jar/go_module - Lexicon — verbatim builtin/rule definitions (
subinclude,sandbox, …) - Commands —
plz build/test/run/query,--include/--exclude - FAQ — motivation, philosophy, Bazel/Buck comparison,
Gochoice src/query/changes.go— affected-target computation- Sibling deep-dives: Bazel · Buck2 · Pants · Cargo · Turborepo · pnpm · npm · Go (
go.work) · uv; the umbrella survey index and the D async/dublandscape