diff --git a/rig/.gitignore b/rig/.gitignore index 63df84c..3e559e1 100644 --- a/rig/.gitignore +++ b/rig/.gitignore @@ -6,14 +6,7 @@ def .env.local ctrl/.env -# generated: the .dot is a build artifact rendered from arch/*.json, never hand-edited. -# The .svg IS committed — onboarding material should render in a repo browser. -arch/*.dot -# ctrl/Tiltfile.gen was here for a generator that no longer exists. ctrl/Tiltfile -# is now a real, committed file that derives its values when Tilt parses it, so -# there is nothing generated to ignore. - -# binaries pulled by `make deps-bundle` for the air-gapped installer image +# the manifests the air-gapped installer image unpacks (docs/notes/Dockerfile.deps.md) vendor # Overlays that live with no version control of their own, or clones of their own diff --git a/rig/.version b/rig/.version new file mode 100644 index 0000000..17e51c3 --- /dev/null +++ b/rig/.version @@ -0,0 +1 @@ +0.1.1 diff --git a/rig/BOOTSTRAP.md b/rig/BOOTSTRAP.md index bb65495..059ef0d 100644 --- a/rig/BOOTSTRAP.md +++ b/rig/BOOTSTRAP.md @@ -83,9 +83,8 @@ fetches with `curl`. A stock `debian:trixie-slim` has no curl — detection runs fine, then the first download dies with `curl: command not found` and an exit code of 127. That is the bootstrap paradox `ctrl/Dockerfile.deps` exists to kill — the installer carries its own toolchain so the host needs only Docker — -but building the image and running it are two different things, and only the -build has a Makefile target today. **On a genuinely bare machine, run it by -hand:** +but `make deps image` only builds the image; running it is the step below. +**On a genuinely bare machine, run it by hand:** ```bash make deps image # builds rig-deps:deps @@ -207,13 +206,13 @@ never tracks it), or keep it anywhere else and name it by path. ## Run it ```bash -make kind-up # idempotent create, then selects the context -make tilt-up # context + your assigned port +make cluster up # idempotent create, then selects the context +make tilt # context + your assigned port ``` -`tilt-up` passes `--context kind-` every time, which is the point of going +`make tilt` passes `--context kind-` every time, which is the point of going through `make` at all: tilt cannot deploy into whichever cluster you last looked at. -`make tilt-down` and `make kind-down` close the loop, and `make kind-reset` is +`make tilt down` and `make cluster down` close the loop, and `make cluster reset` is delete-and-recreate for when a cluster wedges. diff --git a/rig/README.md b/rig/README.md index 7fd247e..58ea60f 100644 --- a/rig/README.md +++ b/rig/README.md @@ -1,200 +1,99 @@ # rig -A runnable local model of a large, regulated estate — legacy and new side by -side. Its job is onboarding and exploration, not a production replica: most -services are deliberately mocked, because what has to be faithful is the -topology, not the workloads. +rig builds a disposable Kubernetes environment on your machine, so you can see how a system +fits together without access to any of it. It is for **onboarding and exploration**, not a +production replica. -## Prerequisite - -**Docker.** Nothing else — no curl, no jq, no python, no apt repositories. - -### Starting from plain Windows - -Everything here is bash and runs *inside* a Linux shell, so on a Windows machine -that means WSL. Nothing in rig installs WSL, and nothing will: `wsl --install` -enables Windows features and requires a reboot, which is not something a script -should do to a machine on your behalf — and there is no tested undo for it. - -From an elevated PowerShell or Command Prompt, once: +Most services in it are **not real**. What has to be faithful is the topology: names, ports, +dependency order, who can reach whom, and how it fails. That is what makes a whole estate fit on +a laptop. A real 20-service platform does not fit once in 14 GB; stubs of about 30 MB each fit +three times. -```powershell -wsl --install -``` +Under the hood it is a kind cluster, a local image registry and a Tilt dev loop, each with its +own name and ports, so several environments run side by side. What runs on it is an +**overlay**: a folder outside rig that holds one use case. -Then reboot and open the Linux shell it installed. +## Using rig from a project -**If you cloned this on the Windows side, copy it into WSL before carrying on.** -WSL can reach the Windows drives at `/mnt/c`, and working from there mostly -functions — slowly — but file watching does not: that filesystem raises no -inotify events, so anything watching for edits silently stops seeing them. +You don't need to read rig to use it. Get a release (`rig-.tar.gz`, or a checkout at +the tag `rig-`), extract it anywhere, and point the project at it (`RIG_DIR`). Then run +`make check`: it says what the machine still lacks and which command comes next. `.version` +names the release; a project records the one it was last run against. See +[`docs/notes/overlay.md`](docs/notes/overlay.md). -```bash -cp -r /mnt/c/Users//rig ~/rig -cd ~/rig -``` +## Prerequisite -`make deps` reports it if you are running from `/mnt/...`. Then carry on below. +**Docker.** Nothing else: no curl, jq, python or apt repositories. -If it fails, the usual causes give unhelpful messages: +On Windows, rig runs inside WSL. Run `wsl --install` once, from an elevated shell, then reboot. +Work inside the Linux filesystem, not `/mnt/c`: file watching doesn't work there, and +`make deps` warns about it. Git Bash, MSYS and Cygwin don't work (no `/proc`, no docker socket). | symptom | cause | | --- | --- | | "the virtual machine could not be started" | virtualization disabled in BIOS/UEFI | -| the command is not recognised | Windows build too old — needs 2004 or later | +| the command is not recognised | Windows build too old (needs 2004 or later) | | the install starts, then nothing works | a reboot is still pending | -Running the scripts from **Git Bash, MSYS or Cygwin does not work** — those look -close enough to a Linux shell to get started and then fail without `/proc` or a -docker socket. `ctrl/deps.sh` detects that and says so rather than letting you -find out the slow way. - -## Read the docs first - -```bash -make docs # serves on localhost, prints the URL -``` - -They run before anything is installed, which matters because they are the -instructions for everything else. No cluster and no toolchain required. - -Why the code is the way it is — the reasoning, measurements and gotchas — lives -in [`docs/notes/`](docs/notes/), one file per script, so the code keeps short comments. - -## Then - -```bash -make check # is this machine ready? short; `make check all` for every detail -make deps # install the toolchain (add `core` on a managed machine) -make cluster up # cluster + registry + the profile's addons -``` +On a machine with nothing but Docker, [BOOTSTRAP.md](BOOTSTRAP.md) runs the toolchain through +the installer container. -That is the whole setup. `make cluster up` also starts this environment's local -registry and wires it into the node, so an image built locally is pullable by the -cluster without going near docker.io. `make check` shows its port, among -everything else: +## What it does -```bash -make check # ... registry localhost: (running) -docker build -t localhost:/app:1 . -docker push localhost:/app:1 -kubectl --context kind-$(basename $PWD) run app --image=localhost:/app:1 -``` +| command | does | +| --- | --- | +| `make docs` | serves the docs locally; works before anything is installed | +| `make check` | is this machine ready? memory, toolchain, cluster, the next command (`check all` for every detail) | +| `make deps` | installs the pinned toolchain (`deps core` on a managed machine) | +| `make cluster up` | the cluster, its registry and the configured addons (`down`, `reset`, `list`, `free`) | +| `make tilt` | the dev loop: rig's part, then the overlay's Tiltfile | +| `make selftest` | checks rig itself | +| `make help` | every target | -The port block is derived from the directory name, so two copies of rig never -collide — nothing to configure. `make check all` lists it; `bash ctrl/ports.sh persist` -pins it into `ctrl/.env` if you want it fixed: +Every verb is a `Makefile` target calling one script in `ctrl/`. The aliases (`kind-up`, +`tilt-up`, …) call the same scripts. -```bash -make cluster list # every cluster on this machine, with memory -make cluster free # stop the others if memory is tight -make cluster down # remove this cluster and its registry -``` +## Overlays -**The verbs are yours to change.** `cluster` is the script — `ctrl/cluster.sh` — -and every spelling above is a `Makefile` target that calls it. `make kind-up` is -an alias for `make cluster up`, kept because the other projects on this machine -answer to that spelling and muscle memory spans repos rather than stopping at -one. Nothing outside the `Makefile` reads these names, so rename them, drop the -ones you never type, or add whatever your own projects already say: each alias -is two lines at the bottom of the file, calling the same script the canonical -target does. - -**`make tilt` works on a fresh copy, unedited.** rig's `ctrl/Tiltfile` does -rig's part — identity, context guard, registry, the manifests — and then includes -the overlay's own Tiltfile. With no overlay named that is `examples/starter`, so -the dev loop comes up with its two examples running and nothing to configure. - -It hardcodes nothing. It asks `ctrl/ports.sh active` for this environment's -cluster name, kube context, ports and paths — the same values every other rig -script resolves through `ctrl/lib/config.sh` — so a copied and renamed rig, or a -moved overlay, deploys into its own cluster with no edits. - -`make help` lists every target. `make selftest` checks rig itself, the installer's -detection included; `make selftest install` runs the installer on clean containers -([`docs/notes/installer-testing.md`](docs/notes/installer-testing.md)). - -On a machine where Docker really is the only thing installed, `make deps` has -nothing to download with — see [BOOTSTRAP.md](BOOTSTRAP.md), which runs the -toolchain through the installer container and carries on to scaffolding and running -a new project. - -## What runs is an overlay - -rig is the machine: toolchain, cluster, registry, port block, the dev loop's -plumbing. What runs on it is an **overlay** — one folder, outside rig's version -control, holding a use case: its settings (`rig.env`), its manifests -(`k8s/overlays/dev`), its images and Tiltfile, its addons, its kind config if it -needs its own. rig reads it and never writes into it. See -[`docs/notes/overlay.md`](docs/notes/overlay.md). +An overlay holds its settings (`rig.env`), its manifests (`k8s/overlays/dev`), its Tiltfile and +images, its addons, and its kind config if it needs one. rig reads it and never writes into it. ```bash -cp -r examples/starter local/myenv # local/ is gitignored -OVERLAY=local/myenv make cluster up # or OVERLAY=local/myenv in ctrl/.env +cp -r examples/starter local/myenv # local/ is ignored +OVERLAY=local/myenv make cluster up OVERLAY=local/myenv make tilt ``` -An overlay can also be a repo of its own, anywhere, or a project folder that -carries rig at `/rig/` with a three-line forwarding Makefile. +With no overlay named, rig runs `examples/starter`. [`examples/data`](examples/data/) adds +postgres, redis and airflow as plain manifests. A project can carry rig at `./rig`, or keep it +anywhere and name it with `RIG_DIR`. -## One environment per folder +The cluster name, context, image tags and ports derive from the overlay folder's name (rig's +own, with no overlay), so environments never collide, and one's teardown never touches another. -Cluster name, kubectl context, image tags and the host port block all derive -from a folder name — the overlay's when one is named, else rig's own — so copies -never collide and neither one's teardown can touch the other. Two overlays run -side by side from one rig; two copies of rig do too. +## Configuration -## Profiles +Layers, weakest first: built-in defaults < `ctrl/versions.env` < the profile < the overlay's +`rig.env` < `ctrl/.env` < the command line. A profile (`ctrl/env.d/`) is optional and says how +this machine reaches the world: `mirror` (a pull-through cache) and `offline` (air-gapped) ship +as examples. The cluster itself is one kind template: the overlay's, or rig's one-node default. -**rig needs no profile.** With none named it runs on built-in defaults: one node, -no addons, a local registry, the newest Kubernetes version it pins. A profile is -an optional file in `ctrl/env.d/`, named by `PROFILE`, that says how this machine -reaches the world. rig ships two as **examples**; copy one to use it (the copy is -gitignored): +rig's own addons make the cluster work: `metallb` (LoadBalancer addresses), `cert-manager` (a +local CA), `metrics-server` (`kubectl top`). A workload's database, cache or scheduler belongs to +its overlay. There is no ingress controller: services are reached through LoadBalancer +addresses, or through the overlay's own gateway on the environment's HTTP port. -| example | what it changes | -| --- | --- | -| `mirror.env.example` | images through a pull-through cache of an internal registry | -| `offline.env.example` | air-gapped: everything from a preloaded local registry | +## Where things are -```bash -cp ctrl/env.d/mirror.env.example ctrl/env.d/mirror.env -PROFILE=mirror make cluster up ``` - -The layers, weakest first: built-in defaults < `ctrl/versions.env` < the profile -< the overlay's `rig.env` < `ctrl/.env` < your command line. - -The **cluster itself** is one file: the overlay's `kind-config.yaml.tpl` if it has -one, else rig's `ctrl/k8s/kind-config.yaml.tpl` (one node). To change it — more -nodes, other port mappings, mounts — edit it and `make cluster reset`. The node -count is read back out of it, so there is nothing to drift. - -## Addons - -Each addon is its own idempotent script, and `ADDONS` names the ones to install, -in order. Adding one is adding a file — there is no dispatcher to edit. An -overlay's `addons/.sh` is found before rig's own. - -**There is no ingress controller, deliberately.** They pin a narrow window of -Kubernetes versions, so depending on one would constrain which k8s a rig can be -built with — and running a trailing-edge control plane is often the point. -Services are reached through MetalLB and `type: LoadBalancer`, which carries no -such constraint and is also what a real cluster does. - -rig's own addons make the cluster work: - -| Addon | Does | -| --- | --- | -| `metallb` | gives `type: LoadBalancer` an address it can actually reach | -| `cert-manager` | a local CA, so TLS works offline | -| `metrics-server` | makes `kubectl top` work on kind | - -What a workload needs — a database, a cache, a scheduler — belongs to its -overlay. [`examples/data`](examples/data/) carries postgres, redis and airflow as -plain manifests (no helm: a chart repo is a network dependency), with passwords -generated on first install and kept across re-runs: - -```bash -OVERLAY=examples/data make cluster up +Makefile the verbs +.version this release: patch and minor steps are edits here; milestones get a tag +ctrl/ one script per verb; lib/config.sh (settings and layers); Tiltfile (rig's + half of the dev loop); k8s/kind-config.yaml.tpl; addons/; env.d/; versions.env +docs/ index.html (make docs); notes/, one per script: why the code is as it is +examples/ starter (the default overlay), data (postgres, redis, airflow) +standalone/ single-file kits generated from ctrl/ (make standalone); never edited by hand +tests/ the installer's host fixtures +BOOTSTRAP.md from a bare machine with only Docker +STALE.md assumptions withdrawn, each checked by selftest ``` diff --git a/rig/VERSION b/rig/VERSION deleted file mode 100644 index 6e8bf73..0000000 --- a/rig/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.1.0 diff --git a/rig/ctrl/.env.example b/rig/ctrl/.env.example index 50ce021..bf6f6ee 100644 --- a/rig/ctrl/.env.example +++ b/rig/ctrl/.env.example @@ -1,5 +1,5 @@ # Machine-local config. Copy to ctrl/.env (gitignored) and edit. -# Cluster SHAPE: an optional profile in ctrl/env.d/. Architecture MODEL: arch/.json. +# Cluster SHAPE: an optional profile in ctrl/env.d/. # Notes: docs/notes/env.md # A profile in ctrl/env.d/ to build. Empty means rig's built-in defaults, which diff --git a/rig/docs/index.html b/rig/docs/index.html index bc95964..f67d298 100644 --- a/rig/docs/index.html +++ b/rig/docs/index.html @@ -386,7 +386,8 @@ docker load < rig.tgz && make cluster up PROFILE=offline

Those addresses are reachable from the Linux side, including inside WSL. A - browser on Windows has no route to them: use the ingress host ports instead.

+ browser on Windows has no route to them: use the environment's HTTP port instead, where + an overlay's gateway listens (its kind config maps it to the host).

Networking and memory

The cluster uses kind's built-in networking, which does enforce standard @@ -417,9 +418,9 @@ OVERLAY=local/myenv make cluster up

With none named, rig runs its own examples/starter. examples/data carries postgres, redis and airflow as an overlay's own - addons. A project can also carry rig at <project>/rig/ and be - the overlay itself, with a three-line forwarding Makefile — see - docs/notes/overlay.md.

+ addons. A project can carry rig at ./rig, or keep it anywhere and name + it with RIG_DIR. .version names the release; the project + records the one it was last run against. See docs/notes/overlay.md.

What runs in it

Each component is one of three things, and switching between them is a diff --git a/rig/docs/notes/Dockerfile.deps.md b/rig/docs/notes/Dockerfile.deps.md index 8a62db2..0ab9132 100644 --- a/rig/docs/notes/Dockerfile.deps.md +++ b/rig/docs/notes/Dockerfile.deps.md @@ -15,7 +15,7 @@ docker build -f ctrl/Dockerfile.deps --target deps -t -deps . docker build -f ctrl/Dockerfile.deps --target deps-full -t -deps:full . ``` -`deps-full` bakes every pinned binary in at build time, and the manifests rig's own addons apply (metallb, cert-manager, metrics-server). `docker save` it and you have the whole installer as one file to carry into an air-gapped network. There, put the manifests where the addons look for them: +`make deps image full` builds the second. `deps-full` bakes every pinned binary in at build time, and the manifests rig's own addons apply (metallb, cert-manager, metrics-server). `docker save` it and you have the whole installer as one file to carry into an air-gapped network. There, put the manifests where the addons look for them: ``` docker run --rm -v "$PWD/vendor:/out/vendor" rig-deps:full manifests --to /out/vendor/manifests diff --git a/rig/docs/notes/Makefile.md b/rig/docs/notes/Makefile.md index 6f8f872..fdb9bab 100644 --- a/rig/docs/notes/Makefile.md +++ b/rig/docs/notes/Makefile.md @@ -31,7 +31,7 @@ Read positionally, so the order is a contract; ctrl/selftest.sh pins it. The two `OVERLAY` and `CLUSTER` given as make arguments (`make tilt OVERLAY=local/x`) are handed to that `$(shell ...)` explicitly. Before make 4.4, `$(shell)` runs with make's own environment and does not see command-line variables, while the recipes do: tilt would then be told one context and the Tiltfile would guard on another. An overlay's forwarder avoids the question by putting `OVERLAY` in the environment. -This used to be sed over ctrl/.env plus a slug computed in the Makefile, which is a SECOND derivation of values lib/config.sh already owns, and the two could disagree about the port after `ports.sh persist`, or about the name for any directory whose sanitised form differs from its raw one. One source now; the Tiltfile reads the same line. +One source: lib/config.sh owns these values, and the Tiltfile reads the same line. A second derivation in the Makefile could disagree about the port after `ports.sh persist`, or about the name of any directory whose sanitised form differs from its raw one. ## CLUSTER / KCTX fallback @@ -39,7 +39,7 @@ The fallback matters: ports.sh sources config.sh, and if a profile or .env is br ## ARGS as .PHONY -Words after the target become the script's subcommand; each gets a no-op rule so make does not treat them as goals. They are also marked PHONY, because some of those words name real directories. `cfg`, `ctrl`, `docs`, `gen` and `init` all exist at this level, and make considers a target that is an existing directory already built, so `make build ctrl` ran the build and then printed "make: 'ctrl' is up to date". The empty rule is not enough on its own; only .PHONY stops make consulting the filesystem. +Words after the target become the script's subcommand; each gets a no-op rule so make does not treat them as goals. They are also marked PHONY, because some of those words name real directories (`ctrl`, `docs`, `examples`), and make considers a target that is an existing directory already built: without it, `make docs ctrl` would print "make: 'ctrl' is up to date". The empty rule is not enough on its own; only .PHONY stops make consulting the filesystem. ## tilt: --port guard diff --git a/rig/docs/notes/check.md b/rig/docs/notes/check.md index f4d9d8a..713c436 100644 --- a/rig/docs/notes/check.md +++ b/rig/docs/notes/check.md @@ -54,7 +54,7 @@ switch for the host part, so the standalone `rigdeps.sh detect` is short too. rig knows what a node costs (NODE_MB), not what a workload does, so the parts come from the overlay's rig.env: `MEMORY_BUDGET="name=MB ..."`, in the order listed; a 0 is left out, and an entry that is not `name=MB` is a `!` line. rig.env is sourced, so an overlay can size a part by -its own knob (poc_rig's airflow follows its mode). When one part outweighs the rest together +its own knob (an airflow sized by the mode the overlay runs it in). When one part outweighs the rest together (and is at least 1 GB), check names it as the first thing to switch off; `MEMORY_BUDGET_TIP` says how, in the overlay's words. No budget: one `?` row says where to add it. diff --git a/rig/docs/notes/env.md b/rig/docs/notes/env.md index a2c4b4e..77bbb72 100644 --- a/rig/docs/notes/env.md +++ b/rig/docs/notes/env.md @@ -2,7 +2,7 @@ ## ctrl/.env.example: header -Machine-local config. Copy to ctrl/.env (gitignored) and edit. The cluster SHAPE is an optional profile in ctrl/env.d/ — see the *.env.example there. The architecture MODEL lives in arch/.json — not in .env either. +Machine-local config. Copy to ctrl/.env (gitignored) and edit. The cluster SHAPE is an optional profile in ctrl/env.d/ — see the *.env.example there. ## ctrl/.env.example: CLUSTER diff --git a/rig/docs/notes/overlay.md b/rig/docs/notes/overlay.md index 3276b65..6886340 100644 --- a/rig/docs/notes/overlay.md +++ b/rig/docs/notes/overlay.md @@ -168,13 +168,16 @@ No search path: an unset `RIG_DIR` means `./rig`, and nothing else is tried. Mov changes nothing in the cluster's name, context or ports; moving or renaming the overlay folder does. -`VERSION`, at rig's top, names the rig. It travels with the folder, vendored, a -worktree or a plain copy, and needs no git. A project records the one it was last run -against and warns on another; it does not refuse: +`.version`, at rig's top, names the rig. It travels with the folder, vendored, a +worktree, a plain copy or a `git archive` release, and needs no git. Patch and minor +steps are edits to that file, with no tag; a `rig-` tag marks a milestone only. +A project records the version it was last run against and warns on another; it does not +refuse. Releases before 0.1.1 carry `VERSION` instead, so the project reads that as a +fallback: ```make -RIG_REF := 0.1.0 -RIG_AT := $(strip $(shell cat '$(RIG_DIR)/VERSION' 2>/dev/null)) +RIG_REF := 0.1.1 +RIG_AT := $(strip $(shell cat '$(RIG_DIR)/.version' 2>/dev/null || cat '$(RIG_DIR)/VERSION' 2>/dev/null)) ifneq ($(RIG_AT),$(RIG_REF)) $(warning rig at $(RIG_DIR) is $(or $(RIG_AT),unversioned); this overlay was last run against rig $(RIG_REF)) endif @@ -183,18 +186,3 @@ endif `rig.env` holds no secrets, by this contract. A repository whose `.gitignore` has a broad `*.env` (a common secrets rule) would still hide it, so an overlay living in such a repo re-includes it in its own `.gitignore`: `!rig.env`. - -## Moving a copied rig to an overlay - -A rig copied into a project and edited there splits cleanly: - -| was, in the copy | goes to | -| --- | --- | -| `ctrl/k8s/base`, `ctrl/k8s/overlays` | `k8s/` | -| the workload parts of `ctrl/Tiltfile` | `Tiltfile` (paths now relative to the overlay) | -| `ctrl/env.d/.env` | `rig.env` | -| edits to `ctrl/k8s/kind-config.yaml.tpl` | `kind-config.yaml.tpl` (`${HOST_WORKDIR}` → `${OVERLAY_DIR}`) | -| workload addons | `addons/` | -| Dockerfiles for the workload | beside the Tiltfile | -| `ctrl/.env` | `rig/ctrl/.env` (this machine's; drop a `MANIFESTS_DIR=ctrl/k8s/overlays/dev` line) | -| everything else of rig's | replaced by rig as it is | diff --git a/rig/docs/notes/selftest.md b/rig/docs/notes/selftest.md index d4a34cb..74be170 100644 --- a/rig/docs/notes/selftest.md +++ b/rig/docs/notes/selftest.md @@ -28,7 +28,7 @@ So the loop is generated FROM the list: add a key to `CONFIG_OVERRIDABLE` and th ## one derivation, not three -The Makefile used to compute the cluster name itself and sed `TILT_PORT` out of `ctrl/.env`: a second derivation of values `lib/config.sh` already owns, which could disagree with it after `ports.sh persist`. It now reads `ports.sh active`. Nothing structurally prevents the sed coming back, so the agreement is asserted against the real `make -n` output rather than against the source. +The Makefile reads `ports.sh active` rather than deriving the cluster name and `TILT_PORT` itself: a second derivation of values `lib/config.sh` already owns could disagree with it after `ports.sh persist`. Nothing structurally prevents one coming back, so the agreement is asserted against the real `make -n` output rather than against the source. `--no-print-directory` and a grep, not `tail -1`: run from `make selftest` this is a RECURSIVE make, and the "Entering/Leaving directory" lines go to STDOUT. `tail -1` then reads "make[1]: Leaving directory ..." and both checks fail, but only when invoked through make, never when the script is run directly. A test that passes one way and fails the other is worse than no test.