Skip to content

Proposal: structured Go build and linker flag compatibility #2111

Description

@cpunion

Summary

LLGo should parse Go build/compiler/linker flags into typed compiler and linker options instead of merely registering selected flags and then silently dropping most of them.

This is an umbrella proposal. The first implementation step is Go-compatible handling of -ldflags -s and -w. Later PRs can add the remaining linker flags and the other Go build/tool flags without repeatedly changing command plumbing or inventing flag-specific forwarding paths.

Motivation and current behavior

Today the flag path is incomplete and inconsistent:

  • llgo build records the flags registered by PassBuildFlags, but run and cmptest currently register and discard them, while test and install do not register the full set.
  • -ldflags can be accepted by the command line and passed to package loading, but LLGo does not parse its linker options and they do not affect the final native link.
  • -gcflags only has partial handling for a few frontend flags and does not implement Go's package-pattern and last-match semantics.
  • unsupported flags can therefore appear to succeed while producing a binary with different semantics from go build.

That is particularly surprising for release builds such as:

llgo build -ldflags='-s -w' ./cmd/app

Go behavior to preserve

The Go build flags -asmflags, -gcflags, -gccgoflags, and -ldflags accept:

[package-pattern=]quoted argument list

They may be repeated. If multiple entries match a package, the last matching entry wins. An entry without a pattern applies to command-line packages. Quotes group an argument but are not shell expansion.

For Go 1.26 cmd/link:

  • -w omits DWARF.
  • -s omits the native symbol table and implies -w only when -w was not explicitly set.
  • Therefore -s -w=false requests no native symbol table but retains DWARF where the backend supports that combination.
  • -s does not remove Go build information, required dynamic symbols, or Go's pclntab. Runtime stack traces, runtime.Caller, and runtime.FuncForPC continue to use pclntab.

References:

Proposed architecture

1. Capture flags consistently

All commands that build or link code (build, install, run, test, and cmptest) should preserve every occurrence in command-line order and pass it to internal/build.Config. go/packages may still receive the subset it needs, but it must not be the storage or parsing layer for LLGo semantics.

2. Parse per-package argument lists once

Introduce a reusable parser with Go-compatible behavior for:

  • optional package patterns;
  • single and double quoted arguments;
  • repeated flags and last-match-wins selection;
  • boolean forms such as -s, -s=true, -s=false, -w, -w=0, and -w=false;
  • options whose value may be joined (-X=pkg.name=value) or separate (-X pkg.name=value).

The final root link gets the options selected for its command-line package. The same representation can later be used for compile and assembly invocations.

3. Translate into typed intent

Do not forward Go linker arguments blindly to clang/lld. Parse them into an LLGo-owned structure, conceptually:

LinkOptions {
    StripSymbols bool
    DWARF        unset | enabled | disabled
    Rewrites     []XDefinition
    External     ExternalLinkOptions
    ...
}

Resolve derived behavior only after parsing all arguments. In particular:

omitDWARF = explicitW if -w was specified
omitDWARF = stripSymbols otherwise

Each object format/backend then maps that intent to supported linker arguments and post-link operations. This avoids leaking ELF flags into Mach-O, PE, wasm, or embedded targets.

4. Report support accurately

Each recognized flag should be classified as:

  • implemented with Go-compatible behavior;
  • accepted as a documented no-op because Go also treats it as one;
  • rejected with a clear llgo: unsupported linker flag ... diagnostic;
  • internal/driver-owned and rejected when supplied directly by a user.

No newly parsed flag should be silently ignored. During migration, flags that LLGo historically accepted but has not implemented may remain compatibility no-ops only when called out explicitly in the implementation issue/PR.

Complete linker-flag support plan

The current Go 1.26 cmd/link surface can be divided into implementation groups:

Group Flags LLGo plan
Size/debug metadata -s, -w, -compressdwarf Implement first; preserve pclntab independently
Go program metadata -X, -buildid, -B Reuse LLGo global rewrite support; add reproducible build-ID/UUID mapping
Link mode/tool selection -linkmode, -extld, -extldflags, -extar, -libgcc, -linkshared, -pluginpath Validate against LLGo's clang/lld and build-mode model; translate per target
Layout/loader policy -D, -E, -H, -I, -L, -R, -T, -r, -funcalign, -randlayout, -bindnow, -aslr, -d Add explicit target-capability mappings; reject unsupported combinations
Instrumentation/security -asan, -msan, -race, -fipso Integrate only with matching LLGo runtime/compiler instrumentation
Validation/link semantics -checklinkname, -strictdups, -pruneweakmap, -k, -g, -f, -e, -h Implement at the LLGo IR/object boundary where meaningful; otherwise diagnose unsupported use
Diagnostics/profiling -V, -v, -c, -dumpdep, -debugnosplit, -debugtextsize, -debugtramp, -benchmark, -benchmarkprofile, -cpuprofile, -memprofile, -memprofilerate, -capturehostobjs Map to LLGo phases rather than passing to the native linker
Driver-owned/output -buildmode, -o, -tmpdir, -importcfg, -installsuffix Keep ownership in the LLGo build driver and detect conflicts
Go compatibility no-op/deprecated -a, -n Match Go's linker no-op behavior, distinct from top-level build -a/-n

The list should be versioned against the Go release used by LLGo CI so a new upstream flag is detected rather than silently falling through.

Other Go flag families

The same parser/dispatch layer should be extended in separate, reviewable PRs:

  1. -gcflags: package selection, -lang, optimization/inlining flags, diagnostics, experiments, and explicit unsupported diagnostics.
  2. -asmflags: Plan 9 assembler options and package selection.
  3. -gccgoflags: either implement for a future gccgo-compatible path or reject clearly; do not pass them to clang by name.
  4. Common go build flags: -a, -n, -p, -race, -msan, -asan, -trimpath, -work, -x, -tags, -buildmode, -buildvcs, -compiler, -installsuffix, -pkgdir, -toolexec, module flags, overlay, and PGO. Each needs an LLGo semantic owner and a capability/error policy.
  5. Low-level go tool compile compatibility should share the typed compiler options but remain a separate CLI contract from go build.

Initial implementation: -s and -w

The first PR should:

  • make -ldflags reach every build/link command path;
  • parse ordered boolean -s/-w values, including explicit false;
  • make -s imply -w only when -w is unset;
  • suppress DWARF production when requested, including when LLGO_DEBUG_SYMBOLS is enabled;
  • preserve LLGo's embedded pclntab/funcinfo and verify that stack names and file/line data still work;
  • introduce the backend/post-link hook where additional native symbols can be removed incrementally;
  • test parser precedence and observable Mach-O/ELF output.

LLGO_DEBUG_SYMBOLS remains a developer escape hatch, but an explicit command-line -w must take precedence. Existing default output should not gain DWARF merely because -w=false is accepted; producing full default Go-style DWARF is a separate compatibility step.

Relationship to configurable pclntab placement

Pclntab placement is intentionally a separate LLGo option and proposal. -s must retain the default embedded pclntab to match Go. A future -pclntab=embedded|external|none controls whether LLGo runtime symbolization metadata is embedded, loaded from a sidecar, or omitted. The proposals are related in post-link ordering, but neither flag should implicitly rewrite the other.

Related proposal: #2112

Compatibility and rollout

  • Preserve current output when none of the new flags is supplied.
  • Start with native Mach-O and ELF; add PE/wasm/embedded mappings behind explicit capability checks.
  • Perform pclntab post-link rewriting/export before any operation that removes the native symbol table.
  • On Darwin, perform final code signing after all binary mutation.
  • Include effective link options in build fingerprints/caches.

Acceptance criteria

  • Parser unit tests cover quoting, repeated values, package patterns, joined/separate values, boolean spellings, invalid values, and last-match behavior.
  • -w, -s, -s -w=false, and later matching overrides have documented observable behavior.
  • -s does not regress panic stack traces, runtime.Caller, runtime.CallersFrames, or runtime.FuncForPC under the default pclntab mode.
  • Mach-O and ELF tests inspect DWARF and symbol-table sections instead of relying only on file size.
  • All build/link commands use one options path.
  • Unsupported combinations fail with actionable diagnostics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Proposal-Acceptedgo-compatCompatibility with Go commands, flags, and toolchain behaviorproposalProposal

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions