Skip to content

builder,interp: fix -ldflags -X not overriding variables with default values#5345

Closed
abgoyal wants to merge 1 commit intotinygo-org:devfrom
abgoyal:fix-ldflags-x-override
Closed

builder,interp: fix -ldflags -X not overriding variables with default values#5345
abgoyal wants to merge 1 commit intotinygo-org:devfrom
abgoyal:fix-ldflags-x-override

Conversation

@abgoyal
Copy link
Copy Markdown
Contributor

@abgoyal abgoyal commented Apr 24, 2026

Currently, -ldflags "-X pkg.Var=value" fails to override string variables that have a default value in source code:

var Version = "dev"  // -X override is ignored, stays "dev"
var Version string   // -X override works correctly

In standard Go, -X works in both cases. This patch brings TinyGo's behavior in line with Go.

The root cause is the timing of when -X values are applied relative to the interp pass. The previous approach:

  1. Erase global, make it undefined
  2. Run interp (executes Version = "dev" from init code)
  3. Link in -X value via makeGlobalsModule

By step 2, interp had already stored the default value, and the linking step couldn't reliably override it.

This patch changes the approach:

  1. Set -X value as initializer before interp runs
  2. Run interp, but skip stores to -X globals
  3. Global retains the -X value

This also means dependent compile-time computations work correctly:

var Version = "dev"
var VersionLen = len(Version)  // now evaluates with -X value

With this fix, makeGlobalsModule becomes redundant - we no longer need a separate module to work around type renaming issues since we set the value directly on the global using its existing type during per-package compilation.

I've removed makeGlobalsModule as part of this change. If there are edge cases I haven't considered where keeping it would be safer, happy to restore it as a fallback.

… values

Currently, -ldflags "-X pkg.Var=value" fails to override string variables
that have a default value in source code:

    var Version = "dev"  // -X override is ignored, stays "dev"
    var Version string   // -X override works correctly

In standard Go, -X works in both cases. This patch brings TinyGo's
behavior in line with Go.

The root cause is the timing of when -X values are applied relative to
the interp pass. The previous approach:

  1. Erase global, make it undefined
  2. Run interp (executes `Version = "dev"` from init code)
  3. Link in -X value via makeGlobalsModule

By step 2, interp had already stored the default value, and the linking
step couldn't reliably override it.

This patch changes the approach:

  1. Set -X value as initializer before interp runs
  2. Run interp, but skip stores to -X globals
  3. Global retains the -X value

This also means dependent compile-time computations work correctly:

    var Version = "dev"
    var VersionLen = len(Version)  // now evaluates with -X value

With this fix, makeGlobalsModule becomes redundant - we no longer need
a separate module to work around type renaming issues since we set the
value directly on the global using its existing type during per-package
compilation.

I've removed makeGlobalsModule as part of this change. If there are edge
cases I haven't considered where keeping it would be safer, happy to
restore it as a fallback.
@abgoyal abgoyal marked this pull request as ready for review April 24, 2026 05:54
Comment thread builder/build.go
// Erase all globals that are part of the undefinedGlobals list.
// This list comes from the -ldflags="-X pkg.foo=val" option.
// Instead of setting the value directly in the AST (which would
// mean the value, which may be a secret, is stored in the build
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.

I think this is a common use case for -ldflags. It is a major implication of this change, have to think about it. I am looking to see if we can use some of this proposed change and still prevent this from being cached...

@deadprogram
Copy link
Copy Markdown
Member

@abgoyal please see #5346 as an alternative solution.

@abgoyal
Copy link
Copy Markdown
Contributor Author

abgoyal commented Apr 24, 2026

I had noticed the build cache comment while implementing the patch. also saw #5346 and makes sense to me as a less invasive change. I will close my PR in favor of #5346 if thats more in line with the existing code flow.

I do have a question, separately: I am not sure I understand the secrets-in-build-cache issue. Won't that same secret be in the binary as well? And regular go also seems to not mind leaving them in the build cache. Could you please help me understand?

@deadprogram
Copy link
Copy Markdown
Member

deadprogram commented Apr 24, 2026

I do have a question, separately: I am not sure I understand the secrets-in-build-cache issue. Won't that same secret be in the binary as well? And regular go also seems to not mind leaving them in the build cache. Could you please help me understand?

It is a very common pattern for device provisioning to generate a unique binary per device with whatever config data embedded right into the binary.

Is embedding unprotected secrets into a binary a good idea? Probably not a good idea.

But perhaps even worse is having those secrets or any other provisioning data left on the machine used to perform the build, if for no other reason than it being unexpected; you would think to protect the build artifacts themselves. Scanning caches looking for secrets is possible if you have that data in the cache.

Anyhow, hopefully that explains some of the current thinking.

@abgoyal abgoyal closed this Apr 24, 2026
@aykevl
Copy link
Copy Markdown
Member

aykevl commented Apr 24, 2026

Suggestion for an alternative approach: remove the value from the AST before the SSA step runs. That way it is left unset and can be overridden.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants