make build exits 0 without building anything #110

Closed
opened 2026-08-09 19:19:10 +02:00 by clawbot · 2 comments
Collaborator

make build reports success and produces no binary.

$ rm -f vaultik && make build; echo "EXIT=$?"
make: Nothing to be done for 'build'.
EXIT=0
$ ls vaultik
ls: cannot access 'vaultik': No such file or directory

Makefile:1 lists build in .PHONY, but no build: rule exists
the real target is vaultik: at line 70. Declaring it phony is what
converts the error you would otherwise get ("No rule to make target") into
a silent success: make considers a phony target with no prerequisites and
no recipe already satisfied.

Found during the PR #109
review as an out-of-scope observation, then reproduced. Pre-existing —
Makefile is byte-identical to main across that PR.

Why this matters more than a missing alias

make build is the documented convention for building in this org, so
this is the command a person or agent reaches for first. It exits 0. A
caller that checks the exit code — CI, a script, an agent verifying its
own work — concludes the build succeeded. Nothing is produced and nothing
says so.

That is the same failure shape this repo has now closed in five other
places: #78 (linter version skew), #85 (Docker layer cache), #93 (Go test
cache), #88/#99 (shared lint cache), each a gate reporting a green it did
not earn. This one is in the build target itself.

Definition of done

  1. make build builds the binary. Simplest correct fix is
    build: vaultik, keeping vaultik: as the file rule.
  2. rm -f vaultik && make build produces the binary and exits 0; the
    negative control is that a deliberately broken build makes it exit
    non-zero. Verify both — a target that cannot fail is no better than one
    that cannot build.
  3. Audit the rest of .PHONY for the same defect: any name listed there
    without a corresponding rule silently succeeds. Fix or remove each.
  4. script/cibuild exits 0.

Note

.PHONY on this line also lists all, local, install, docker,
hooks, deps and others. I have not checked whether each has a rule —
item 3 covers it, and any that do not have the identical silent-success
behaviour.

`make build` reports success and produces no binary. ``` $ rm -f vaultik && make build; echo "EXIT=$?" make: Nothing to be done for 'build'. EXIT=0 $ ls vaultik ls: cannot access 'vaultik': No such file or directory ``` `Makefile:1` lists `build` in `.PHONY`, but **no `build:` rule exists** — the real target is `vaultik:` at line 70. Declaring it phony is what converts the error you would otherwise get ("No rule to make target") into a silent success: make considers a phony target with no prerequisites and no recipe already satisfied. Found during the [PR #109](https://git.eeqj.de/sneak/vaultik/pulls/109) review as an out-of-scope observation, then reproduced. Pre-existing — `Makefile` is byte-identical to `main` across that PR. ## Why this matters more than a missing alias `make build` is the documented convention for building in this org, so this is the command a person or agent reaches for first. It exits 0. A caller that checks the exit code — CI, a script, an agent verifying its own work — concludes the build succeeded. Nothing is produced and nothing says so. That is the same failure shape this repo has now closed in five other places: #78 (linter version skew), #85 (Docker layer cache), #93 (Go test cache), #88/#99 (shared lint cache), each a gate reporting a green it did not earn. This one is in the build target itself. ## Definition of done 1. `make build` builds the binary. Simplest correct fix is `build: vaultik`, keeping `vaultik:` as the file rule. 2. `rm -f vaultik && make build` produces the binary and exits 0; the negative control is that a deliberately broken build makes it exit non-zero. Verify both — a target that cannot fail is no better than one that cannot build. 3. Audit the rest of `.PHONY` for the same defect: any name listed there without a corresponding rule silently succeeds. Fix or remove each. 4. `script/cibuild` exits 0. ## Note `.PHONY` on this line also lists `all`, `local`, `install`, `docker`, `hooks`, `deps` and others. I have not checked whether each has a rule — item 3 covers it, and any that do not have the identical silent-success behaviour.
clawbot added this to the 1.0.0 milestone 2026-08-09 19:19:10 +02:00
Author
Collaborator

Plan

Implementing this together with
issue #108 as one PR
against main.

  1. Add build: vaultik as the description suggests, keeping vaultik:
    as the file rule so incremental builds still work.
  2. Verify both directions by hand and report both:
    rm -f vaultik && make build produces the binary and exits 0, and a
    deliberately broken source tree makes the same command exit non-zero.
  3. .PHONY audit, already done against the current Makefile: the 19
    names listed are all, bootstrap, setup, check, test,
    lint, lint-fix, fmt, fmt-check, build, clean, deps,
    test-coverage, local, install, release, release-snapshot,
    docker, hooks. Every one of them has a rule except build, so
    build is the only instance of this defect. vaultik is correctly
    absent from .PHONY, being a real file target.
  4. A regression guard so the audit does not have to be redone by hand:
    a test that parses the Makefile, extracts the .PHONY names and
    the set of declared rules, and fails on any phony name with no rule.
    It fails on the current tree (build) and passes once the rule is
    added, which is also how the audit stays true for names added later.

The guard is a parse rather than an invocation of make on purpose:
make test is what runs it, so shelling back into make build from
inside it would nest the build in the test and drop a binary into the
tree as a side effect of testing. The one thing a parse cannot prove —
that the recipe can still fail — is the manual negative control in
item 2.

## Plan Implementing this together with [issue #108](https://git.eeqj.de/sneak/vaultik/issues/108) as one PR against `main`. 1. Add `build: vaultik` as the description suggests, keeping `vaultik:` as the file rule so incremental builds still work. 2. Verify both directions by hand and report both: `rm -f vaultik && make build` produces the binary and exits 0, and a deliberately broken source tree makes the same command exit non-zero. 3. `.PHONY` audit, already done against the current `Makefile`: the 19 names listed are `all`, `bootstrap`, `setup`, `check`, `test`, `lint`, `lint-fix`, `fmt`, `fmt-check`, `build`, `clean`, `deps`, `test-coverage`, `local`, `install`, `release`, `release-snapshot`, `docker`, `hooks`. Every one of them has a rule except `build`, so `build` is the only instance of this defect. `vaultik` is correctly absent from `.PHONY`, being a real file target. 4. A regression guard so the audit does not have to be redone by hand: a test that parses the `Makefile`, extracts the `.PHONY` names and the set of declared rules, and fails on any phony name with no rule. It fails on the current tree (`build`) and passes once the rule is added, which is also how the audit stays true for names added later. The guard is a parse rather than an invocation of `make` on purpose: `make test` is what runs it, so shelling back into `make build` from inside it would nest the build in the test and drop a binary into the tree as a side effect of testing. The one thing a parse cannot prove — that the recipe can still fail — is the manual negative control in item 2.
Author
Collaborator

Implemented in
PR #111, on branch
fix-prune-json-and-build.

What was built

build: vaultik added, with vaultik: kept as the file rule.

.PHONY audit (item 3)

All 19 names on that line were checked against the rules in the file:
all, bootstrap, setup, check, test, lint, lint-fix, fmt,
fmt-check, build, clean, deps, test-coverage, local,
install, release, release-snapshot, docker, hooks.

build was the only one without a rule. Every other name has one,
so nothing else needed fixing or removing. vaultik is correctly absent
from .PHONY, being a real file target — putting it there would have
made the binary rebuild unconditionally.

A regression guard keeps that true without a re-audit by hand:
TestPhonyTargetsAllHaveRules parses the Makefile, extracts the
.PHONY names and the set of declared rules, and fails on any phony
name with no rule. A companion test asserts build reaches the rule
that produces the binary, so giving build: an empty recipe of its own
would not satisfy it. Both fail on the tree before this change.

The guard parses rather than invoking make: make test is what runs
it, so shelling back into make build would nest a build inside the
test run and drop a binary into the tree as a side effect of testing.

Both directions (item 2)

$ rm -f vaultik && make build; echo "EXIT=$?"
go build -ldflags "..." -o vaultik ./cmd/vaultik
EXIT=0
$ ls -la vaultik
142829863 bytes

Negative control, with a deliberate syntax error added to a file in
cmd/vaultik:

$ rm -f vaultik && make build; echo "EXIT=$?"
cmd/vaultik/zz_deliberate_break.go:4:34: syntax error: unexpected name is at end of statement
make: *** [Makefile:82: vaultik] Error 1
EXIT=2
$ ls vaultik
ls: cannot access 'vaultik': No such file or directory

So the target both builds and can fail. The broken file was removed
before committing and is not in the branch.

Item 4

script/cibuild exits 0. The three check RUN layers each echoed the
fresh CHECK_EPOCH, so make fmt-check, make lint and make test
executed rather than replaying from cache; the only CACHED layers are
below the ARG CHECK_EPOCH line.

One reporting note for whoever reads the gate next: make test now
prints 16 ok lines rather than 15. cmd/vaultik had no test file
until this change and was reported as ? no test files; the Makefile
guard lives there, beside the Makefile it guards, so the package now
compiles and runs tests.

Implemented in [PR #111](https://git.eeqj.de/sneak/vaultik/pulls/111), on branch `fix-prune-json-and-build`. ## What was built `build: vaultik` added, with `vaultik:` kept as the file rule. ## `.PHONY` audit (item 3) All 19 names on that line were checked against the rules in the file: `all`, `bootstrap`, `setup`, `check`, `test`, `lint`, `lint-fix`, `fmt`, `fmt-check`, `build`, `clean`, `deps`, `test-coverage`, `local`, `install`, `release`, `release-snapshot`, `docker`, `hooks`. **`build` was the only one without a rule.** Every other name has one, so nothing else needed fixing or removing. `vaultik` is correctly absent from `.PHONY`, being a real file target — putting it there would have made the binary rebuild unconditionally. A regression guard keeps that true without a re-audit by hand: `TestPhonyTargetsAllHaveRules` parses the `Makefile`, extracts the `.PHONY` names and the set of declared rules, and fails on any phony name with no rule. A companion test asserts `build` reaches the rule that produces the binary, so giving `build:` an empty recipe of its own would not satisfy it. Both fail on the tree before this change. The guard parses rather than invoking `make`: `make test` is what runs it, so shelling back into `make build` would nest a build inside the test run and drop a binary into the tree as a side effect of testing. ## Both directions (item 2) ``` $ rm -f vaultik && make build; echo "EXIT=$?" go build -ldflags "..." -o vaultik ./cmd/vaultik EXIT=0 $ ls -la vaultik 142829863 bytes ``` Negative control, with a deliberate syntax error added to a file in `cmd/vaultik`: ``` $ rm -f vaultik && make build; echo "EXIT=$?" cmd/vaultik/zz_deliberate_break.go:4:34: syntax error: unexpected name is at end of statement make: *** [Makefile:82: vaultik] Error 1 EXIT=2 $ ls vaultik ls: cannot access 'vaultik': No such file or directory ``` So the target both builds and can fail. The broken file was removed before committing and is not in the branch. ## Item 4 `script/cibuild` exits 0. The three check `RUN` layers each echoed the fresh `CHECK_EPOCH`, so `make fmt-check`, `make lint` and `make test` executed rather than replaying from cache; the only `CACHED` layers are below the `ARG CHECK_EPOCH` line. One reporting note for whoever reads the gate next: `make test` now prints **16** `ok` lines rather than 15. `cmd/vaultik` had no test file until this change and was reported as `? no test files`; the Makefile guard lives there, beside the Makefile it guards, so the package now compiles and runs tests.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/vaultik#110