Complete project infrastructure tooling and automation.
Stand up a complete project, install its system dependencies, fetch and run its tools, build it into a wheel — all from small primitives that don't own your project. Declare what you need in TOML; the tooling pulls the rest on demand.
|
A fast ephemeral script runner: fetches, runs, discards. One curl line installs it, and everything else in the org is reachable through it — no Python, no Docker, no clone. . <(curl -sSL https://just-buildit.github.io/get-jb.sh) # the only curl you need
jbx install-deps # system packages, from bootstrap.toml
jbx just-bashit:logging log "hello" # any script in the org, by name
jbx gh:user/repo/tool # or anything on GitHub, by URLNamespaces, PEP 723 inline deps, cache TTL, checksum verification and a
sandboxed env are all built in. |
The toolchain — each piece stands alone, and they compose:
| Name | Role | Get it |
|---|---|---|
| just-bashit | Proven bash scripts & tools | jbx just-bashit:logging log "hello" |
just-makeit (jm) |
Python C extensions out-of-the-box | pip install just-makeit |
| just-buildit | Zero-dep PEP 517 build backend for C extensions | pip install just-buildit |
Greenfield Python+C extension — just-makeit new stands up a
complete project: C source, headers, CMakeLists, Python bindings, type
stubs, tests, and benchmarks — all green on the first make test.
The build backend is just-buildit; the bootstrap manifest
(bootstrap.toml, carrying the [tools.*] table and the system build
deps under [dev.*]) is dropped in pre-populated so the next
contributor lands running. You write the algorithm; nothing else.
Any project, anywhere — drop a bootstrap.toml at the repo root and
run jbx install-deps. System packages for apt/pacman/brew/dnf/zypper/msys2
are detected and installed. No Python, no Docker, no setup.
One-off scripts — jbx <name> fetches, runs, and discards. Detects
bash or Python from shebang/extension; Python scripts pick up PEP 723
inline dependencies through uv run. Cache TTL, checksum verification,
function dispatch, sandboxed env — all there.
Namespaces — jbx [NAMESPACE:]NAME. A namespace resolves to a base
URL. The default is just-buildit, served from the org-pages root with
a curated aliases.toml. jbx install-deps just works; jbx gh:user/repo/tool hits GitHub raw directly.
Every repo in the org answers the same targets, because every repo vendors the
same standard.mk. You do not
have to read a project's Makefile to work on it:
make help # every target, generated from the rules — never hand-listed
make setup # one-time per clone: dependencies + the git hook
make install-deps # system packages, from bootstrap.toml
make test # the default suite
make lint # the gate CI runs — and CI runs nothing else
make format # auto-fix with every configured formatterAdopting it is one fetch and one include. Vendor the file:
curl -fsSL https://just-buildit.github.io/standard.mk -o standard.mk…then your Makefile is configuration only — feature flags and the commands behind them, never a copy of the shared rules:
# Makefile
TEST_CMD = pytest
TEST_FAST_CMD = pytest -x
CLEAN_PATHS = dist/ build/
include standard.mkThat is the whole adoption, and it is enough for make help, test, lint,
format and clean. Nine feature flags add the rest — HAS_C, HAS_PYTHON,
HAS_RUST, HAS_DOCS, HAS_DOXYGEN, HAS_BENCH, HAS_COVERAGE,
HAS_RELEASE, HAS_EXAMPLES — and each requires the command behind it: a flag
with nothing to run is a parse error, not a target that silently does nothing.
Each file owns one concern: the Makefile says how a tool runs,
pyproject.toml says which version, and .pre-commit-config.yaml says
when — dispatching back in with make lint-<tool> so a hook and CI cannot
disagree about what a check means.
Vendoring arms a drift gate: make lint re-fetches canonical every time and
fails on any difference, so the copy in your repo cannot quietly become a fork.
# Install jbx (and just-runit, the same runner's full name)
. <(curl -sSL https://just-buildit.github.io/get-jb.sh)
# Install just-makeit, then stand up a Python+C extension
# (drops a bootstrap.toml with build deps pre-populated under [dev.*])
pip install just-makeit
just-makeit new my_project --object engine --state gain:double:1.0
cd my_project
# Install those build deps for your platform (apt/pacman/brew/dnf/...)
jbx install-deps
# Build and test — green on first run
make && make testMIT across all repos.
Below the fold: planning notes for contributors. External readers can stop here.
bootstrap.toml— what must exist BEFORE the language ecosystem's own package manager can run: system packages, and thejbxtools a project depends on. Lives at repo root in every project type (Python, C, bare).pyproject.tomlkeeps its packaging job; this keeps the bootstrap job. Named for what it declares, not for a tool —jbreads as just-buildit, the PEP 517 backend, which never opens it. It carries both halves: the declarative system-package list grouped by purpose (runtime,dev) and package manager (apt,pacman,brew,dnf,zypper,msys2), and the[tools.*]table.jb.tomlandjb-deps.tomlare the deprecated former names, still read with a warning.- Namespaced invocation —
jbx [NS:]NAME. A namespace resolves to a single base URL. Default namespace =just-buildit. Built-in prefixes:just-bashit:,gh:,https://. aliases.toml— manifest at the org-pages root mapping short names to URLs.jbx some-toolconsults the alias table when there is no script at${NS_URL}/some-tool[.sh|.py].install-deps.sh— thin per-project shim that delegates tojbx install-deps. Optional —jbx install-depsworks directly whenbootstrap.tomlis present.
[project]
name = "my_project"
version = "0.1.0"
[tools.install-deps]
source = "just-bashit:install-deps"
groups = ["runtime", "dev"]
[tools.just-makeit]
source = "just-bashit:just-makeit"
config = "just-makeit.toml"
[runtime.apt] packages = ["libzmq3-dev", "libfftw3-dev"]
[runtime.pacman] packages = ["zeromq", "fftw"]
[dev.apt] packages = ["build-essential", "cmake", "python3-dev"]
[dev.pacman] packages = ["base-devel", "cmake", "python"][aliases]
install-deps = "https://just-buildit.github.io/jbs/install-deps.sh"
get-jb = "https://just-buildit.github.io/get-jb.sh"
# Third-party tools welcomed via PR.- Explicit
NS:prefix —jbx gh:user/repo/x,jbx https://...: skip everything below, resolve directly. [tools.NAME]inbootstrap.toml(walking up from CWD) — use declared source.- Default namespace
aliases.toml— fetch (cached), look upNAME. - Default namespace direct hit — HEAD
${NS_URL}/NAME.sh, then.py. - Error — name not found.
-
jb/jbx/just-builditnaming; conflict detection forjb; stalejr/jxcleanup on reinstall — superseded by the rename below;jbandjust-builditare no longer installed as runner names -
just-runitsubcommand dispatch (falls through to the SPEC runner;jbxnever takes subcommands) - Namespace model: bare NAME → default NS via
aliases.tomlthen HEAD probe;just-bashit:NAMEco-fetch - Arg parsing: flags not captured as FUNC; FUNC validated via
declare -F; verbose shadowing diagnostic - Version-aware installer: fresh/upgrade/already-current;
JB_REINSTALL=1escape hatch - Org-pages site: themed,
aliases.toml, mirror CI,get-jb.shshort URL -
bootstrap.tomlformat defined; doppler carries one -
jbs-deps.tomlauto-discovery in CWD
- Rename
jbs-deps.toml→jb-deps.tomlacross just-bashit source, docs, doppler - Rename
jb.toml/jb-deps.toml→bootstrap.toml, andjb/just-builditstop naming the runner — one token had meant the org, the PEP 517 backend, and the script runner at once. Old names still read, with a warning (removal: just-bashit#30) -
just-runit install— readsbootstrap.toml, walks up from CWD, pre-fetches every declared tool into cache -
just-makeit newemitsbootstrap.tomlwith dev deps pre-populated — shipped in just-makeit 0.57.0 (just-makeit#936, closing #935) - User namespace config —
~/.config/just-runit/namespaces.tomlfor custom NS registration
- Parity
get-just-*.shscripts — addget-just-makeit.sh,get-just-bashit.sh,get-just-buildit.sh -
just-buildit init [--pep517|--bare|--c]— unified scaffold entry point -
bootstrap.tomlschema — JSON Schema for editor completion - CHANGELOG hygiene across repos is uneven
One standard.mk every repo includes, with per-repo variation expressed as
configuration rather than as a fork. Design RFC and full rationale:
doppler-dsp/doppler#555.
Scope: all repos in just-buildit and doppler-dsp.
Canonical home: just-buildit.github.io, served at
https://just-buildit.github.io/standard.mk and hand-edited beside
aliases.toml. It must not live in a repo that consumes the standard, which
rules out doppler, just-makeit and just-buildit/just-buildit — the last
of those has a Makefile of its own, so it is an adopter like any other. The
org-pages root is a non-consumer whose stated charter is already "the small
static resources the toolchain depends on", and serving from the CDN keeps the
drift gate to a single curl with no clone, no auth, and no raw.githubusercontent
rate limit — the same reason the jbs/ libs were moved there.
A written convention exists (skills://makefile-convention) and every repo
hand-implements it, so they drift. Measured 2026-07-30:
| doppler | just-makeit | |
|---|---|---|
| targets defined | 50 | 27 |
| targets shared between the two | 18 | 18 |
listed by make help |
30 | 22 |
CI run: steps invoking make |
12 / 83 | 4 / 71 |
Concrete consequences, all live: doppler has no format target; the same
benchmark concepts are named bench-baseline/bench-check in one repo and
bench-save/bench-compare in the other; zensical build --strict is
implemented in three places that disagree, and every doppler PR builds the docs
site twice; make wheel is in .PHONY and in help with no rule, so it exits
0 having done nothing.
Universal (9): all help setup clean test test-fast lint format install-deps,
plus one lint-<tool> dispatch target per configured tool. Feature groups
defined only when flagged — HAS_DOCS, HAS_C, HAS_DOXYGEN, HAS_PYTHON, HAS_RUST,
HAS_BENCH, HAS_COVERAGE, HAS_RELEASE, HAS_EXAMPLES — plus test-all / gates
aggregates.
Cap, with every flag on: 35 user-facing + N dispatch + 3 enforcement. The
three counts are separate because they scale differently — N is per-repo (the
tools that repo configures) and the enforcement gates are fixed — and stating
them as one number is how a cap stops being a check. It had already happened
here: this line read "38 targets", the P0 prototype measured 38, and the two
were different sets of 38. The plan was counting lint-<tool> and not the
enforcement gates, and P0 counted the reverse, so two offsetting errors agreed.
The 35 is the #555 group
table summed: 9 universal + 2 aggregates + 24 across the nine groups
(HAS_DOCS 3, HAS_C 3, HAS_DOXYGEN 2, HAS_PYTHON 3, HAS_RUST 1,
HAS_BENCH 3, HAS_COVERAGE 2, HAS_RELEASE 6, HAS_EXAMPLES 1). Check a
build against that table target-for-target, not in total — summing to the
right number is what hid this. Repo-local targets (LOCAL_TARGETS) are outside
the cap by definition: they are what criterion 1 bounds.
install-deps is universal (system packages via jbx install-deps; a no-op
where a repo declares none) and is distinct from setup, which installs
project deps. test-examples sits in HAS_EXAMPLES. Both were added after
the list was checked against the measured union rather than assembled from
memory — see criterion 10.
- Dispatch is required, not optional.
.pre-commit-config.yamlcallsmake -s lint-<tool>; the Makefile invokes$(DEV_RUN) <tool>;uv.lockpins the version. This is what makes local and CI resolve identically, so a hand-pinnedadditional_dependencieslist becomes unnecessary. helpis generated from##comments, never hand-maintained.releaseis reserved for the C build type (clean+build BUILD_TYPE=Release). The release workflow isship/tag-release.- Naming is
<noun>-<qualifier>, making the noun a namespace:test-python,test-rust,version-check.make test-<TAB>then completes the whole family. local.mkis included if present and may only add targets, never redefine a standard one — otherwise it becomes the fork this prevents.- The drift gate fetches canonical every time, and a failed fetch fails the
gate.
make lintcompares the vendored copy against https://just-buildit.github.io/standard.mk. There is no cache: a cache would mean the most likely failure — the fetch failing while the network is fine (CDN outage, a bad deploy, a 404 after a rename) — silently degrades into "compared against something older", and one bad deploy would disable the drift gate across every repo at once with nothing going red. A gate that cannot reach its reference has not passed; it has not run, and it says so by failing. That is the same reason the gate fails rather than warns.
Each file owns exactly one concern; nothing states a tool's invocation twice.
| File | Purpose |
|---|---|
Makefile |
Configuration only — feature flags, path and tool overrides, include standard.mk, and repo-local targets. Nothing shared lives here. |
standard.mk |
The shared targets, vendored verbatim by fetching canonical (see below). Never edited in-repo: the drift gate fails make lint on any difference from canonical. |
local.mk |
Optional. Included if present; may only add targets, never redefine a standard one — otherwise it becomes the fork this prevents. |
pyproject.toml |
Which tools, at what versions (the dev group). |
uv.lock |
Pins those versions, committed. This is what makes local and CI resolve identically, and so what lets dispatch close the environment-drift class. |
.pre-commit-config.yaml |
When a check fires. Dispatches inward (entry: make -s lint-<tool>, language: system) and resolves no versions for lock-managed tools. Non-Python tools that cannot come from uv.lock — clang-format, cmake-format — keep their pinned rev: here. |
bootstrap.toml |
Bootstrap manifest: the [tools.*] table, plus system packages under [dev.<manager>] consumed by install-deps. (jb.toml / jb-deps.toml are the deprecated former names, still read with a warning.) |
.github/workflows/*.yml |
Calls make <target>. Anything else must be provably environment plumbing — runner setup, artifact transport, release packaging. |
Adoption adds exactly one file. Both repos already carry Makefile,
pyproject.toml, uv.lock, .pre-commit-config.yaml and bootstrap.toml today;
only standard.mk is new, and local.mk is optional and so far unneeded.
Adoption means curl canonical — never cp a sibling.
curl -fsSL -o standard.mk https://just-buildit.github.io/standard.mkThat is the whole of it: there is no line to add, because STANDARD_URL
defaults to canonical inside the file. Vendoring is what arms the drift gate.
The failure mode this rules out is not hypothetical — it is how the second
adopter ended up unguarded on day one. doppler adopted by taking the file from
just-makeit's working tree, which at that moment held the pre-publication copy
where STANDARD_URL was still empty and opt-in. The result passed every gate
and reported standard-check: inert, which reads like a pass, so nothing went
red anywhere: doppler ran for a day with no drift protection at all while
its make lint was green. Fixed by re-vendoring
(doppler-dsp/doppler#559).
Two consequences worth stating, since both cost time:
- Arming-by-default only protects an adopter that vendors the current canonical. A copy taken from another repo carries whatever that repo had at the time, including an older arming policy — so the copy silently reintroduces the fail-open the default exists to prevent.
- After a change to canonical, adopters do not update themselves. Their gate goes red, which is correct and is the point; the fix is to re-fetch, never to edit the vendored copy. An adopter whose gate is inert is the one case that will not tell you.
Values are doppler / just-makeit, against the 2026-07-30 baseline above. Status as of 2026-07-31, every number re-measured rather than carried forward:
| # | criterion | baseline | target | now | |
|---|---|---|---|---|---|
| 1 | repo Makefile holds only config + genuinely local targets | 50 / 27 | 0 shared | 0 shared / 0 shared | ✅ |
| 2 | make help lists every target, and every listed target exists |
60% / 81% | 100% / 100% | 100% / 100% | ✅ |
| 3 | ghost targets (.PHONY with no rule) |
1 / 0 | 0 / 0 | 0 / 0 | ✅ |
| 4 | CI run: steps are make <target> or environment plumbing |
12/83 / 4/71 | 100% / 100% | 6 / 42 unclassified | ⬜ |
| 5 | zensical build --strict implementations |
3 | 1 | 1 | ✅ |
| 6 | docs site builds per doppler PR | 2 | 1 | 1 | ✅ |
| 7 | hand-pinned additional_dependencies for lock-managed tools |
yes | none | none | ✅ |
| 8 | editing vendored standard.mk fails make lint |
n/a | both repos | both | ✅ |
| 9 | make <standard target> behaves identically across repos |
no | yes | byte-identical | ✅ |
| 10 | targets shared by two or more adopting repos that sit outside the standard | 3 | 0 | 0 | ✅ |
Nine of ten met. The one that is not:
Criterion 4 — P4, and honestly measured for the first time. doppler is at
6 unclassified of 74 (3 CI-native, 3 release-path deferred on purpose);
just-makeit at 42 of 71, most of them artifact.yml scaffold-smoke steps that
drive the jm CLI rather than the repo's own build. See P4 below.
Criterion 1 is met in both repos under its corrected reading (see the
property note below): neither Makefile defines a shared target, and every
local one is declared in LOCAL_TARGETS — doppler 26 = 26, reconciliation
enforced by help-check.
Criterion 9 is now stronger than "behaves identically": both repos vendor the
same file, byte for byte (77fe8941…), verified against canonical rather
than against each other.
Criterion 1 is a property, not a count: no shared target defined in a repo
Makefile, and every local one declared in LOCAL_TARGETS. It was originally
written as "≤18 / ≤1", but a local-target count is not a quality signal — a
repo legitimately grows local targets (doppler added seven binary-hygiene gates
in a single afternoon), and a criterion that goes stale on healthy activity
gets ignored rather than fixed. What must not drift is criterion 10, the
shared set.
Criteria 2, 3 and 8 are enforced by gates rather than by review, so they cannot
regress silently — which is the point, since none of the problems above were
decided, they accumulated. make wheel had been exiting 0 with no rule behind
it in a repo that already had a make lint gate, CI on every PR, and a help
entry advertising it: every human control was in place, and none of them caught
it.
Criterion 10 applies the same lesson to the standard's own scope. Three targets
shared by both repos — release-branch, test-examples, install-deps — were
each missed while the list was written from memory, and each was found by
recomputing the union from the two Makefiles. The list is therefore derived by
script from the measured union of adopting repos, and the invariant "no target
shared by two or more repos sits outside the standard" is checked rather than
reasoned about.
-
P0 — prototype
standard.mkin just-makeit; vendored, drift gate inert until P1 publishes canonical. Also collapses just-makeit'sinstall(uv sync --group dev) intosetup, of which it is a strict subset, so a fourth deps-ish name never reaches the standard (just-makeit) — done, just-buildit/just-makeit#636. Prototyping in one repo before publishing paid for itself twice: the file rejected the GNU make 3.81 that macOS ships, and the gates parsed amake -pdatabase whose wording changed in 3.82. Both would have gone out to every adopter at once. P1 must publish the post-review file, which also carriescoverage-gate,HAS_EXAMPLESreduced totest-examples, andalldefaulting tobuildwhereHAS_C. -
P1 — publish canonical
standard.mkin this org; wire the drift gate live (just-buildit) — done, just-buildit.github.io#9 serving at https://just-buildit.github.io/standard.mk, vendored back into just-makeit by just-makeit#637. The gate is proven against the real remote, in CI as well as locally: a matching copy passes, a one-character edit fails with the diff.One design change from the plan.
STANDARD_URLdefaults to canonical insidestandard.mkrather than being set per adopter, because opt-in fails open: a repo that vendors the file and forgets the line has no drift protection and reports that as a notice reading like a pass, and nobody greps for an absent line. Vendoring the file is what arms it; deliberate opt-out isSTANDARD_URL =, a line that exists and so can be found.Consequence, stated because every adopter inherits it:
make lintnow requires network. That is the RFC's tradeoff for refusing a cache, not a side effect — a Pages outage reddenslintorg-wide rather than silently comparing against something older. -
P2 — doppler port:
docs-checkfirst (deletes the three-way divergence and the double site build in one commit), thenlint-<tool>dispatch, then ghost/backfill/renames (doppler) — done, doppler-dsp/doppler#558 plus four follow-ups (#559 re-vendor, #560 criterion 6, #561 the CI-only gates, #562 the binary gates). 50 hand-maintained targets became 0 shared plus 25 genuinely doppler's own;helpwent from 30-of-50 (advertising two that did not work) to 67-of-67 generated.Two things the port could not express, both worked around in the repo rather than by editing the vendored copy, and both feeding the next canonical change:
docs-checkcannot accumulate. The strict build is its own recipe line, so a build failure aborts the target — fatal where the gate exists to report every failure in one pass with a link checker last. Worked around withDOCS_CHECK_BUILD_CMD = :.- The build step has no flag surface.
CMAKE_FLAGSreaches configure only, so an adopting C repo silently drops from N jobs to one. doppler would have gone 20 → 1. Covered byexport CMAKE_BUILD_PARALLEL_LEVEL, which reaches everycmake --buildrather than one recipe.
And a trap worth the plan carrying: doppler adopted by copying just-makeit's working tree, which held the pre-publication file, so it shipped with the gate inert — reporting a notice that reads like a pass — and ran a full day unguarded with
make lintgreen. See Adoption meanscurlcanonical above; that rule exists because of this. -
P3 — convention doc updated to match, landing with P2 — until
standard.mkexists, the doc describing the old names is still accurate (doppler) — done,skills://makefile-conventionrewritten alongside #558, plus the four sibling skills that named the renamed targets. -
P4 — CI port per repo: call standard targets, delete each inline duplicate in the same commit (per repo) — in progress, and the measured state differs sharply by repo:
repo run:stepsmakeplumbing unclassified doppler 74 29 39 6 just-makeit 71 5 24 42 doppler is substantially done: every gate in
ci.ymlanddocs.ymlnow calls a target, including four that had never been runnable outside CI (abi-check,link-check,glibc-check,specan-check) and the doc gates (test-stubs,test-api-docs,test-snippets). Of its remaining 6, three are CI-native (apt inside the Debian container, theneeds.*.resultaggregator,cmake --installpackaging) and three are release-path — deliberately deferred, because those steps only execute during a release, so an error there is invisible until it is expensive. Best ported when a release will exercise them immediately.just-makeit is largely unstarted. Most of its 42 are
artifact.yml's scaffold-smoke steps, which drive the jm CLI rather than the repo's own build — whether those are "plumbing" or want targets is the per-repo judgement P4 asks for, not an omission.Two duplications P4 surfaced that a target-count audit would have missed, both in doppler and both now fixed:
docs.ymlreimplementedmake gen-c-apiinline and had already diverged (uv run mkdocsvsuv run --group docs mkdocs, and acpthat merged where the target replaces — so a deleted C symbol's page survived forever in the deployed site while vanishing locally); andmake test-pythonselected a different set of tests from CI's pytest, which is the RFC's own complaint surviving the port that was supposed to end it.
- Not a rewrite of target semantics — the convention already defines them; this implements them once instead of N times.
- Not removing repo-specific targets. doppler keeps
specan,gallery,record-demo,blazingand its bench scripts. - Not a general fix for environment drift. Dispatch closes it for lock-managed Python tools; anything resolved outside the lock is still on its own.
bootstrap.tomlbeats stdin when both are present. TTY detection ([ -t 0 ]) doesn't survivebash -cor CI — file-first is the only reliable ordering.Filename prefix isSuperseded: the prefix named a tool, which is the mistakejb-, notjbs-— the deps file is an org-level convention, not a just-bashit-specific one.bootstrap.tomlcorrects. The reasoning survives the reversal — it is why the new name is org-level too.Superseded: neither name is installed now.jbmay conflict (e.g. Jenkins X usedjx;jbcould be taken too). Installer detects this and falls back tojust-buildit, which is always installed and unique.get-jb.shstopped creating thejbandjust-builditsymlinks and prunes them on upgrade, so there is nothing left to collide — andjust-builditwas the worse fallback anyway, being the build backend.jbxis the runner —just-runitfor the subcommand form,jbxfor fast one-liners. Both always installed.bootstrap.tomlis standalone, not a[tool.*]table inpyproject.toml. Uniform across project types; Python packaging metadata stays uncoupled from cross-org tooling.--pep517delegates instead of re-implementing scaffolding.just-makeitalready does this well.- Templates as files, not Python heredocs. Diffs against real generated projects stay readable.
