Skip to content
rlsbl.ldflags_symbols
On this page

Does every -X importpath.Symbol=value linker flag name a symbol the Go source declares, and one the linker can set? A -X naming nothing links silently.

#rlsbl.ldflags_symbols

#rlsbl.ldflags_symbols

Does every -X importpath.Symbol=value linker flag name a symbol that exists?

go build -ldflags "-X importpath.Symbol=value" overwrites a package-level string variable at link time. When the named symbol does not exist, the link succeeds silently and the flag does nothing -- no error, no warning, no diagnostic anywhere in the toolchain. A build configuration that says -X main.Version=... while the Go source declares var version therefore ships binaries that report their fallback version forever, and the only way to notice is to run the released binary and read its output.

That is a coupling between two files nothing else compares: the build configuration's chosen symbol name and the Go source's declaration. rlsbl already polices two couplings of the same kind -- go-module-identity (a go.mod module path against the repository's origin identity) and dep-locks (a lockfile against the manifest beside it) -- and this is the third.

What is verified ----------------

Every -X occurrence in the project's tracked build configuration (.goreleaser.yml/.yaml, Makefiles, shell scripts, CI workflow YAML) is resolved to a directory and the symbol is looked up in the Go source there:

  • Error -- the symbol is not declared, or is declared as something the

linker cannot set (a const, a function, a non-string var, a var initialized to a non-constant expression). The linker can only set a package-level var of type string that is uninitialized or initialized to a constant string.

  • Warn -- the symbol exists and can be set, but nothing in the module reads

it. That produces the identical user-visible bug by a different route: the value arrives and the version surface prints a hardcoded literal anyway. Warn rather than error because the fix can require adding a version surface, which is a larger change than a rename.

Symbol-agnostic on purpose --------------------------

The RELATIONSHIP is checked, never a naming convention. main.version with var version and main.Version with var Version are both correct; enforcing either spelling would force churn on working projects and would still miss mismatches in the other direction.

What it refuses to guess ------------------------

An occurrence whose target cannot be resolved is reported as unverified (a note) rather than guessed at: a build-time template in the import path (-X {{ .Env.MODULE }}/cmd/x.Version=...), a package outside this module, and a bare main in a build file that names no package and whose module has several main packages. Guessing there would mint errors against code that is correct.

#Occurrence

One -X flag found in a build file.

#Declaration

What a Go package declares under one name.

#LdflagsVerdict

Result of comparing every -X target against the Go source.

#ok

python
def ok(self)

#git_tracked_files

python
def git_tracked_files(directory)

Every path git tracks under directory, relative to it.

Only tracked files are this project's build configuration: an untracked script is one developer's scratch copy, and a gitignored one is not shipped at all.

#is_build_file

python
def is_build_file(rel_path)

Is rel_path a file that can carry linker flags?

#split_target

python
def split_target(target)

Split importpath.Symbol into its two halves, or (None, None).

The linker splits at the LAST dot, so an import path carrying dots (github.com/owner/repo/internal/version) resolves the same way the toolchain resolves it. A target carrying a build-time template names no directory and is refused here rather than guessed at.

#find_occurrences

python
def find_occurrences(text, rel_path)

Every -X occurrence in text, with 1-based line numbers.

#_normalize_main

python
def _normalize_main(value)

A goreleaser main: value as a directory relative to the module root.

goreleaser accepts a directory (./cmd/tool) or a file (./cmd/tool/main.go); both name the same package.

#goreleaser_builds

python
def goreleaser_builds(text)

[(ldflags_text, main_dir)] for each declared build, or None.

None means the document could not be read as goreleaser's -- the caller then resolves a bare main the way it resolves one in a shell script.

#_explicit_build_target

python
def _explicit_build_target(text, line_index)

The package a go build/go install names, nearest first.

The occurrence's own line is consulted first (the common go build -ldflags "-X main.V=$(V)" ./cmd/tool), then the rest of the file, which covers a Makefile holding its flags in a variable.

#_GoSource

Parsed Go source for one module, with per-directory symbol tables.

#go_files

python
def go_files(self)

Every non-test .go file in the module, module-relative.

vendor/ and testdata/ are other people's code and fixtures; a symbol declared there is not this module's.

#files_in

python
def files_in(self, rel_dir)

The module's non-test Go files directly in rel_dir.

#symbols

python
def symbols(self, rel_dir)

{name: Declaration} for the package-level declarations there.

#string_var_names

python
def string_var_names(self, rel_dir)

#is_read

python
def is_read(self, symbol, decl)

Does anything in the module reference symbol beyond its declaration?

Module-wide rather than package-local, because a symbol injected into a library package (-X .../internal/version.Value) is read from the binary that imports it. A cross-package read is a field_identifier (version.Value), so both identifier kinds count.

#_declarations

python
def _declarations(node, rel_path)

(name, Declaration) pairs for one top-level node.

#_var_spec_declarations

python
def _var_spec_declarations(spec, rel_path)

What one var spec declares, and whether the linker can set it.

#_references

python
def _references(node, wanted, excluded_offsets)

Is wanted used as an identifier anywhere under node?

#_resolve_directory

python
def _resolve_directory(occ, text, builds, module_path, source)

(rel_dir, reason): where the target points, or why it is unverified.

#_main_package_dirs

python
def _main_package_dirs(source)

Module-relative directories declaring package main.

#evaluate_ldflags_symbols

python
def evaluate_ldflags_symbols(module_dirs, *, list_tracked=None)

Compare every -X target in the build configuration against the source.

module_dirs are absolute directories expected to contain a go.mod. list_tracked enumerates the tracked paths under one of them, relative to it; it defaults to :func:git_tracked_files.

Search