Skip to content

buildctl: read a valueless build-arg from the environment - #7030

Merged
tonistiigi merged 1 commit into
moby:masterfrom
ravi-arnan:fix-buildctl-bare-build-arg
Aug 20, 2026
Merged

buildctl: read a valueless build-arg from the environment#7030
tonistiigi merged 1 commit into
moby:masterfrom
ravi-arnan:fix-buildctl-bare-build-arg

Conversation

@ravi-arnan

Copy link
Copy Markdown
Contributor

Closes #7028

--opt build-arg:FOO failed the whole build with invalid opt: invalid value build-arg:FOO instead of taking FOO from the client environment, so callers had to spell out --opt build-arg:FOO=$FOO.

Why this is a consistency fix rather than a new feature

buildctl already sources a build arg from the environment: loadOptEnv (cmd/buildctl/build/util.go) propagates SOURCE_DATE_EPOCH into build-arg:SOURCE_DATE_EPOCH. docker buildx build --build-arg FOO reads the environment as well. The valueless --opt build-arg:FOO was the only form that did not.

Scope

The change is in ParseOpt, not in attrMap. attrMap is shared with ParseLocal, where --local name without a value should keep failing, so relaxing it there would be too broad. There is a second, unrelated copy of attrMap at cmd/buildkitd/main.go:1003 that parses daemon flags; it is deliberately untouched.

Only the build-arg: prefix is affected. A bare --opt target still errors, and so does --opt build-arg: with no name.

A variable that is not present in the environment is left out of the frontend attributes entirely rather than sent as an empty string, so a default in the Dockerfile still applies. That matches buildx.

Verification

New unit tests in cmd/buildctl/build/opt_test.go, 8 cases. The 3 that cover the new behaviour fail on master with exactly the reported error; the 5 regression cases pass on both.

End to end against a real buildkitd (moby/buildkit:latest in a container, reached over docker-container://), using the reporter's Dockerfile:

$ printf 'ARG IMAGE\nFROM $IMAGE\nRUN echo working\n' > Dockerfile
$ export IMAGE=alpine

$ buildctl-master build --frontend dockerfile.v0 --local dockerfile=. --local context=. --opt build-arg:IMAGE
error: invalid opt: invalid value build-arg:IMAGE

$ buildctl-fixed build --frontend dockerfile.v0 --local dockerfile=. --local context=. --opt build-arg:IMAGE
#5 [2/2] RUN echo working
#5 0.118 working
#5 DONE 0.1s

And the unset case, confirming the Dockerfile default is what applies rather than an empty value:

$ printf 'ARG IMAGE=busybox\nFROM $IMAGE\nRUN echo default-applied\n' > Dockerfile
$ unset IMAGE

$ buildctl-fixed build --frontend dockerfile.v0 --local dockerfile=. --local context=. --opt build-arg:IMAGE
#5 [2/2] RUN echo default-applied
#5 0.141 default-applied
#5 DONE 0.2s

gofmt -s, go vet and go test ./cmd/buildctl/build/ are clean.

Docs updated in docs/reference/buildctl.md.

Comment thread cmd/buildctl/build/opt.go Outdated
continue
}
if v, ok := os.LookupEnv(name); ok {
m[opt] = v

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can mess up the order of the what opt wins on duplicates. Should still add to rest.

Comment thread cmd/buildctl/build/opt.go Outdated
rest := make([]string, 0, len(opts))
for _, opt := range opts {
name, ok := strings.CutPrefix(opt, buildArgPrefix)
if !ok || name == "" || strings.Contains(name, "=") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this reads better if conditions are reversed.

@ravi-arnan
ravi-arnan force-pushed the fix-buildctl-bare-build-arg branch from 2aed2d1 to f9503ce Compare August 10, 2026 10:24
@ravi-arnan

Copy link
Copy Markdown
Contributor Author

Both fixed, thanks.

The ordering one was a real bug rather than just a style issue: with --opt build-arg:FOO=bar --opt build-arg:FOO the valueless form came last but lost, because it was written straight into the result map before attrMap ran over the rest. Resolved args now keep their position in the list and go through attrMap like everything else, so last-wins holds in both directions. Added cases for both orders.

Reversing the conditions fell out of the same rewrite.

One case I had to pick a behaviour for, happy to change it: --opt build-arg:FOO=bar --opt build-arg:FOO where FOO is not in the environment. The unset valueless arg is skipped rather than clearing the earlier explicit value, so the result is bar. That keeps "not in the environment means leave it out entirely", but it does mean an unset valueless arg is not a way to unset a previous one. Pinned it with a test so it is at least explicit.

},
{
name: "a build arg missing from the environment is dropped",
opts: []string{"build-arg:IMAGE"},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to be safe these test should clear IMAGE or use some env that is specific enough to never be accidentally defined by env

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, that was a real hole. With IMAGE set in the ambient environment two
cases failed: "a build arg missing from the environment is dropped" picked up the
ambient value, and "a valueless build arg missing from the environment does not
clear an earlier value" returned it instead of busybox.

Extended the existing SOURCE_DATE_EPOCH guard to clear IMAGE as well, so the
cases are hermetic regardless of what the caller has exported. Went with clearing
rather than a more specific variable name so it stays correct if the name is ever
changed.

--opt build-arg:FOO failed the whole build with "invalid value
build-arg:FOO" instead of taking FOO from the client environment, so
callers had to spell out --opt build-arg:FOO=$FOO. "docker buildx build
--build-arg FOO" reads the environment, and buildctl already propagates
SOURCE_DATE_EPOCH from the environment into a build arg in loadOptEnv,
so the valueless form was the odd one out.

Resolve it in ParseOpt rather than in attrMap: attrMap is shared with
--local, where a value is still required. A variable that is not present
in the environment is left out of the frontend attributes entirely
rather than sent as an empty string, so a default in the Dockerfile
still applies.

Signed-off-by: Ravi Arnan <raviarnankeren@gmail.com>
@ravi-arnan
ravi-arnan force-pushed the fix-buildctl-bare-build-arg branch from f9503ce to 92f2efe Compare August 13, 2026 13:41
@tonistiigi
tonistiigi merged commit e09c846 into moby:master Aug 20, 2026
198 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Broken build-arg handling (requires =, ignores environment)

2 participants