From 5b47423c41c6178bb8c8cec8f5af9d0624aac00e Mon Sep 17 00:00:00 2001 From: Matthew Hiles Date: Mon, 15 Jun 2026 14:06:05 -0400 Subject: [PATCH 1/2] Add LinkerFlavor so the odd pairing of COFF+Linux is possible (necessary for UEFI) --- builder/build.go | 25 ++++++-------------- compileopts/config.go | 20 ++++++++++++++++ compileopts/target.go | 3 ++- compileopts/target_test.go | 48 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 19 deletions(-) diff --git a/builder/build.go b/builder/build.go index 5fe1429b85..2c2048e5a5 100644 --- a/builder/build.go +++ b/builder/build.go @@ -842,22 +842,25 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe } ldflags = append(ldflags, "-mllvm", "-mcpu="+config.CPU()) ldflags = append(ldflags, "-mllvm", "-mattr="+config.Features()) // needed for MIPS softfloat - if config.GOOS() == "windows" { - // Options for the MinGW wrapper for the lld COFF linker. + switch config.LinkerFlavor() { + case "coff": + // Options for driving ld.lld in PE/COFF mode. ldflags = append(ldflags, "-Xlink=/opt:lldlto="+strconv.Itoa(speedLevel), "--thinlto-cache-dir="+filepath.Join(cacheDir, "thinlto")) - } else if config.GOOS() == "darwin" { + case "darwin": // Options for the ld64-compatible lld linker. ldflags = append(ldflags, "--lto-O"+strconv.Itoa(speedLevel), "-cache_path_lto", filepath.Join(cacheDir, "thinlto")) - } else { + case "gnu": // Options for the ELF linker. ldflags = append(ldflags, "--lto-O"+strconv.Itoa(speedLevel), "--thinlto-cache-dir="+filepath.Join(cacheDir, "thinlto"), ) + default: + return fmt.Errorf("unknown linker flavor: %s", config.LinkerFlavor()) } if config.CodeModel() != "default" { ldflags = append(ldflags, @@ -1352,14 +1355,6 @@ func determineStackSizes(mod llvm.Module, executable string) ([]string, map[stri } baseStackSize, baseStackSizeType, baseStackSizeFailedAt := functions["tinygo_startTask"][0].StackSize() - // Account for the bytes that tinygo_swapTask pushes onto the goroutine stack - // on every context switch. The static analysis correctly traces Go calls, - // but it cannot see into the assembly-level register push. - var contextSwitchOverhead uint64 - if swapFuncs, ok := functions["tinygo_swapTask"]; ok && len(swapFuncs) == 1 { - contextSwitchOverhead = swapFuncs[0].FrameSize - } - sizes := make(map[string]functionStackSize) // Add the reset handler function, for convenience. The reset handler runs @@ -1408,12 +1403,6 @@ func determineStackSizes(mod llvm.Module, executable string) ([]string, map[stri // overflow will occur even before the goroutine is started. stackSize = baseStackSize } - if stackSizeType == stacksize.Bounded { - // Add the overhead of context switching. This is needed because the - // context switch (tinygo_swapTask) pushes callee-saved registers - // onto the current stack, which is not seen by the static analysis. - stackSize += contextSwitchOverhead - } sizes[name] = functionStackSize{ stackSize: stackSize, stackSizeType: stackSizeType, diff --git a/compileopts/config.go b/compileopts/config.go index 673de40f95..a007c3f4aa 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -466,6 +466,26 @@ func (c *Config) LDFlags() []string { return ldflags } +// LinkerFlavor returns how the configured linker should be driven. +// Usually this is derived from GOOS, but targets may override it explicitly. +func (c *Config) LinkerFlavor() string { + if c.Target.LinkerFlavor != "" { + return c.Target.LinkerFlavor + } + goos := c.GOOS() + if goos == "" { + goos = c.Options.GOOS + } + switch goos { + case "windows": + return "coff" + case "darwin": + return "darwin" + default: + return "gnu" + } +} + // ExtraFiles returns the list of extra files to be built and linked with the // executable. This can include extra C and assembly files. func (c *Config) ExtraFiles() []string { diff --git a/compileopts/target.go b/compileopts/target.go index 70c7047462..42e3734870 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -38,7 +38,8 @@ type TargetSpec struct { Scheduler string `json:"scheduler,omitempty"` Serial string `json:"serial,omitempty"` // which serial output to use (uart, usb, none) Linker string `json:"linker,omitempty"` - RTLib string `json:"rtlib,omitempty"` // compiler runtime library (libgcc, compiler-rt) + LinkerFlavor string `json:"linker-flavor,omitempty"` // how to drive the configured linker (for example: gnu, coff, darwin) + RTLib string `json:"rtlib,omitempty"` // compiler runtime library (libgcc, compiler-rt) Libc string `json:"libc,omitempty"` AutoStackSize *bool `json:"automatic-stack-size,omitempty"` // Determine stack size automatically at compile time. DefaultStackSize uint64 `json:"default-stack-size,omitempty"` // Default stack size if the size couldn't be determined at compile time. diff --git a/compileopts/target_test.go b/compileopts/target_test.go index d8a17a5e34..c700418839 100644 --- a/compileopts/target_test.go +++ b/compileopts/target_test.go @@ -112,3 +112,51 @@ func TestOverrideProperties(t *testing.T) { } } + +func TestConfigLinkerFlavor(t *testing.T) { + tests := []struct { + name string + target *TargetSpec + goos string + want string + }{ + { + name: "default gnu", + target: &TargetSpec{}, + goos: "linux", + want: "gnu", + }, + { + name: "default coff", + target: &TargetSpec{}, + goos: "windows", + want: "coff", + }, + { + name: "default darwin", + target: &TargetSpec{}, + goos: "darwin", + want: "darwin", + }, + { + name: "target override", + target: &TargetSpec{ + LinkerFlavor: "coff", + }, + goos: "linux", + want: "coff", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + config := &Config{ + Options: &Options{GOOS: tc.goos}, + Target: tc.target, + } + if got := config.LinkerFlavor(); got != tc.want { + t.Fatalf("LinkerFlavor() = %q, want %q", got, tc.want) + } + }) + } +} From 3f3551e70d2802daa16a93b99dbbcc2ab1c6b0c8 Mon Sep 17 00:00:00 2001 From: Matthew Hiles Date: Mon, 15 Jun 2026 14:25:10 -0400 Subject: [PATCH 2/2] But back bits of builder.go that were mistakenly removed --- builder/build.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/builder/build.go b/builder/build.go index 2c2048e5a5..24b233263f 100644 --- a/builder/build.go +++ b/builder/build.go @@ -1355,6 +1355,14 @@ func determineStackSizes(mod llvm.Module, executable string) ([]string, map[stri } baseStackSize, baseStackSizeType, baseStackSizeFailedAt := functions["tinygo_startTask"][0].StackSize() + // Account for the bytes that tinygo_swapTask pushes onto the goroutine stack + // on every context switch. The static analysis correctly traces Go calls, + // but it cannot see into the assembly-level register push. + var contextSwitchOverhead uint64 + if swapFuncs, ok := functions["tinygo_swapTask"]; ok && len(swapFuncs) == 1 { + contextSwitchOverhead = swapFuncs[0].FrameSize + } + sizes := make(map[string]functionStackSize) // Add the reset handler function, for convenience. The reset handler runs @@ -1403,6 +1411,12 @@ func determineStackSizes(mod llvm.Module, executable string) ([]string, map[stri // overflow will occur even before the goroutine is started. stackSize = baseStackSize } + if stackSizeType == stacksize.Bounded { + // Add the overhead of context switching. This is needed because the + // context switch (tinygo_swapTask) pushes callee-saved registers + // onto the current stack, which is not seen by the static analysis. + stackSize += contextSwitchOverhead + } sizes[name] = functionStackSize{ stackSize: stackSize, stackSizeType: stackSizeType,