55 "fmt"
66 "os"
77 "os/exec"
8- "path/filepath"
98 "strings"
109)
1110
@@ -81,10 +80,10 @@ func GetBuildCmd(inputFile string, outputFile string, rootFolder string) *exec.C
8180 inputFile ,
8281 )
8382 env := append (os .Environ (), "GOOS=wasip1" , "GOARCH=wasm" , "CGO_ENABLED=0" )
84- // Pin GOTOOLCHAIN to the version declared in the nearest go.mod so the
85- // compiled WASM is reproducible across machines whose local Go differs
86- // from go.mod (GOTOOLCHAIN=auto uses the local Go when it's newer ).
87- if toolchain := goToolchainFromMod (rootFolder ); toolchain != "" {
83+ // Pin GOTOOLCHAIN so the compiled WASM is reproducible. Prefer the
84+ // configured GOTOOLCHAIN; when it's unset, fall back to the version
85+ // declared in the module's go.mod (the local Go version wouldn't pin ).
86+ if toolchain := goToolchain (rootFolder ); toolchain != "" {
8887 env = append (env , "GOTOOLCHAIN=" + toolchain )
8988 }
9089 buildCmd .Env = env
@@ -95,13 +94,37 @@ func GetBuildCmd(inputFile string, outputFile string, rootFolder string) *exec.C
9594 return buildCmd
9695}
9796
98- // goToolchainFromMod returns a GOTOOLCHAIN value (e.g. "go1.26.2") derived
99- // from the nearest go.mod walking up from startDir. Prefers the toolchain
100- // directive when present, falling back to the go directive. Returns "" when
101- // no go.mod or version can be determined.
102- func goToolchainFromMod (startDir string ) string {
103- goModPath := findNearestGoMod (startDir )
104- if goModPath == "" {
97+ // goToolchain returns a GOTOOLCHAIN value (e.g. "go1.26.2") to pin the build
98+ // to. It prefers the configured `go env GOTOOLCHAIN`; when that is unset (e.g.
99+ // "auto") it falls back to the go version declared in the module's go.mod. The
100+ // local Go version is not used as a fallback because it would not pin a
101+ // reproducible toolchain. Returns "" when nothing can be determined.
102+ func goToolchain (dir string ) string {
103+ if v := goEnv (dir , "GOTOOLCHAIN" ); v != "" && v != "auto" {
104+ return v
105+ }
106+ return goToolchainFromMod (dir )
107+ }
108+
109+ // goEnv runs `go env <name>` in dir and returns the trimmed value, or "" on error.
110+ func goEnv (dir , name string ) string {
111+ cmd := exec .Command ("go" , "env" , name )
112+ cmd .Dir = dir
113+ out , err := cmd .Output ()
114+ if err != nil {
115+ return ""
116+ }
117+ return strings .TrimSpace (string (out ))
118+ }
119+
120+ // goToolchainFromMod returns a GOTOOLCHAIN value derived from the go directive
121+ // in the module's go.mod, located via `go env GOMOD`. Returns "" when no go.mod
122+ // or go version can be determined.
123+ func goToolchainFromMod (dir string ) string {
124+ goModPath := goEnv (dir , "GOMOD" )
125+ // `go env GOMOD` returns "" outside a module and os.DevNull when modules
126+ // are disabled (GO111MODULE=off); neither is a real go.mod file.
127+ if goModPath == "" || goModPath == os .DevNull {
105128 return ""
106129 }
107130 f , err := os .Open (goModPath )
@@ -110,47 +133,12 @@ func goToolchainFromMod(startDir string) string {
110133 }
111134 defer f .Close ()
112135
113- var goVersion , toolchain string
114136 scanner := bufio .NewScanner (f )
115137 for scanner .Scan () {
116- line := strings .TrimSpace (scanner .Text ())
117- if line == "" || strings .HasPrefix (line , "//" ) {
118- continue
119- }
120- fields := strings .Fields (line )
121- if len (fields ) < 2 {
122- continue
123- }
124- switch fields [0 ] {
125- case "go" :
126- goVersion = fields [1 ]
127- case "toolchain" :
128- toolchain = fields [1 ]
138+ fields := strings .Fields (scanner .Text ())
139+ if len (fields ) == 2 && fields [0 ] == "go" {
140+ return "go" + fields [1 ]
129141 }
130142 }
131- if toolchain != "" {
132- return toolchain
133- }
134- if goVersion != "" {
135- return "go" + goVersion
136- }
137143 return ""
138144}
139-
140- func findNearestGoMod (startDir string ) string {
141- dir , err := filepath .Abs (startDir )
142- if err != nil {
143- return ""
144- }
145- for {
146- candidate := filepath .Join (dir , "go.mod" )
147- if info , err := os .Stat (candidate ); err == nil && ! info .IsDir () {
148- return candidate
149- }
150- parent := filepath .Dir (dir )
151- if parent == dir {
152- return ""
153- }
154- dir = parent
155- }
156- }
0 commit comments