github.com/fastly/cli@v1.7.2-0.20240304164155-9d0f1d77c3bf/pkg/commands/compute/language_go.go (about) 1 package compute 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "os" 8 "os/exec" 9 "regexp" 10 "strings" 11 12 "github.com/Masterminds/semver/v3" 13 "golang.org/x/mod/modfile" 14 15 "github.com/fastly/cli/pkg/config" 16 fsterr "github.com/fastly/cli/pkg/errors" 17 "github.com/fastly/cli/pkg/text" 18 ) 19 20 // TinyGoDefaultBuildCommand is a build command compiled into the CLI binary so it 21 // can be used as a fallback for customer's who have an existing Compute project and 22 // are simply upgrading their CLI version and might not be familiar with the 23 // changes in the 4.0.0 release with regards to how build logic has moved to the 24 // fastly.toml manifest. 25 // 26 // NOTE: In the 5.x CLI releases we persisted the default to the fastly.toml 27 // We no longer do that. In 6.x we use the default and just inform the user. 28 // This makes the experience less confusing as users didn't expect file changes. 29 const TinyGoDefaultBuildCommand = "tinygo build -target=wasi -gc=conservative -o bin/main.wasm ./" 30 31 // GoSourceDirectory represents the source code directory. 32 const GoSourceDirectory = "." 33 34 // NewGo constructs a new Go toolchain. 35 func NewGo( 36 c *BuildCommand, 37 in io.Reader, 38 manifestFilename string, 39 out io.Writer, 40 spinner text.Spinner, 41 ) *Go { 42 return &Go{ 43 Shell: Shell{}, 44 45 autoYes: c.Globals.Flags.AutoYes, 46 build: c.Globals.Manifest.File.Scripts.Build, 47 config: c.Globals.Config.Language.Go, 48 env: c.Globals.Manifest.File.Scripts.EnvVars, 49 errlog: c.Globals.ErrLog, 50 input: in, 51 manifestFilename: manifestFilename, 52 metadataFilterEnvVars: c.MetadataFilterEnvVars, 53 nonInteractive: c.Globals.Flags.NonInteractive, 54 output: out, 55 postBuild: c.Globals.Manifest.File.Scripts.PostBuild, 56 spinner: spinner, 57 timeout: c.Flags.Timeout, 58 verbose: c.Globals.Verbose(), 59 } 60 } 61 62 // Go implements a Toolchain for the TinyGo language. 63 // 64 // NOTE: Two separate tools are required to support golang development. 65 // 66 // 1. Go: for defining required packages in a go.mod project module. 67 // 2. TinyGo: used to compile the go project. 68 type Go struct { 69 Shell 70 71 // autoYes is the --auto-yes flag. 72 autoYes bool 73 // build is a shell command defined in fastly.toml using [scripts.build]. 74 build string 75 // config is the Go specific application configuration. 76 config config.Go 77 // defaultBuild indicates if the default build script was used. 78 defaultBuild bool 79 // env is environment variables to be set. 80 env []string 81 // errlog is an abstraction for recording errors to disk. 82 errlog fsterr.LogInterface 83 // input is the user's terminal stdin stream 84 input io.Reader 85 // manifestFilename is the name of the manifest file. 86 manifestFilename string 87 // metadataFilterEnvVars is a comma-separated list of user defined env vars. 88 metadataFilterEnvVars string 89 // nonInteractive is the --non-interactive flag. 90 nonInteractive bool 91 // output is the users terminal stdout stream 92 output io.Writer 93 // postBuild is a custom script executed after the build but before the Wasm 94 // binary is added to the .tar.gz archive. 95 postBuild string 96 // spinner is a terminal progress status indicator. 97 spinner text.Spinner 98 // timeout is the build execution threshold. 99 timeout int 100 // verbose indicates if the user set --verbose 101 verbose bool 102 } 103 104 // DefaultBuildScript indicates if a custom build script was used. 105 func (g *Go) DefaultBuildScript() bool { 106 return g.defaultBuild 107 } 108 109 // Dependencies returns all dependencies used by the project. 110 func (g *Go) Dependencies() map[string]string { 111 deps := make(map[string]string) 112 data, err := os.ReadFile("go.mod") 113 if err != nil { 114 return deps 115 } 116 f, err := modfile.ParseLax("go.mod", data, nil) 117 if err != nil { 118 return deps 119 } 120 for _, req := range f.Require { 121 if req.Indirect { 122 continue 123 } 124 deps[req.Mod.Path] = req.Mod.Version 125 } 126 return deps 127 } 128 129 // Build compiles the user's source code into a Wasm binary. 130 func (g *Go) Build() error { 131 var ( 132 tinygoToolchain bool 133 toolchainConstraint string 134 ) 135 136 if g.build == "" { 137 g.build = TinyGoDefaultBuildCommand 138 g.defaultBuild = true 139 tinygoToolchain = true 140 toolchainConstraint = g.config.ToolchainConstraintTinyGo 141 if !g.verbose { 142 text.Break(g.output) 143 } 144 text.Info(g.output, "No [scripts.build] found in %s. Visit https://developer.fastly.com/learning/compute/go/ to learn how to target standard Go vs TinyGo.\n\n", g.manifestFilename) 145 text.Description(g.output, "The following default build command for TinyGo will be used", g.build) 146 } 147 148 if g.build != "" { 149 // IMPORTANT: All Fastly starter-kits for Go/TinyGo will have build script. 150 // 151 // So we'll need to parse the build script to identify if TinyGo is used so 152 // we can set the constraints appropriately. 153 if strings.Contains(g.build, "tinygo build") { 154 tinygoToolchain = true 155 toolchainConstraint = g.config.ToolchainConstraintTinyGo 156 } else { 157 toolchainConstraint = g.config.ToolchainConstraint 158 } 159 } 160 161 // IMPORTANT: The Go SDK 0.2.0 bumps the tinygo requirement to 0.28.1 162 // 163 // This means we need to check the go.mod of the user's project for 164 // `compute-sdk-go` and then parse the version and identify if it's less than 165 // 0.2.0 version. If it less than, change the TinyGo constraint to 0.26.0 166 tinygoConstraint := identifyTinyGoConstraint(g.config.TinyGoConstraint, g.config.TinyGoConstraintFallback) 167 168 g.toolchainConstraint( 169 "go", `go version go(?P<version>\d[^\s]+)`, toolchainConstraint, 170 ) 171 172 if tinygoToolchain { 173 g.toolchainConstraint( 174 "tinygo", `tinygo version (?P<version>\d[^\s]+)`, tinygoConstraint, 175 ) 176 } 177 178 bt := BuildToolchain{ 179 autoYes: g.autoYes, 180 buildFn: g.Shell.Build, 181 buildScript: g.build, 182 env: g.env, 183 errlog: g.errlog, 184 in: g.input, 185 manifestFilename: g.manifestFilename, 186 metadataFilterEnvVars: g.metadataFilterEnvVars, 187 nonInteractive: g.nonInteractive, 188 out: g.output, 189 postBuild: g.postBuild, 190 spinner: g.spinner, 191 timeout: g.timeout, 192 verbose: g.verbose, 193 } 194 195 return bt.Build() 196 } 197 198 // identifyTinyGoConstraint checks the compute-sdk-go version used by the 199 // project and if it's less than 0.2.0 we'll change the TinyGo constraint to be 200 // version 0.26.0 201 // 202 // We do this because the 0.2.0 release of the compute-sdk-go bumps the TinyGo 203 // version requirement to 0.28.1 and we want to avoid any scenarios where a 204 // bump in SDK version causes the user's build to break (which would happen for 205 // users with a pre-existing project who happen to update their CLI version: the 206 // new CLI version would have a TinyGo constraint that would be higher than 207 // before and would stop their build from working). 208 // 209 // NOTE: The `configConstraint` is the latest CLI application config version. 210 // If there are any errors trying to parse the go.mod we'll default to the 211 // config constraint. 212 func identifyTinyGoConstraint(configConstraint, fallback string) string { 213 moduleName := "github.com/fastly/compute-sdk-go" 214 version := "" 215 216 f, err := os.Open("go.mod") 217 if err != nil { 218 return configConstraint 219 } 220 defer f.Close() 221 222 scanner := bufio.NewScanner(f) 223 for scanner.Scan() { 224 line := scanner.Text() 225 parts := strings.Fields(line) 226 227 // go.mod has two separate definition possibilities: 228 // 229 // 1. 230 // require github.com/fastly/compute-sdk-go v0.1.7 231 // 232 // 2. 233 // require ( 234 // github.com/fastly/compute-sdk-go v0.1.7 235 // ) 236 if len(parts) >= 2 { 237 // 1. require [github.com/fastly/compute-sdk-go] v0.1.7 238 if parts[1] == moduleName { 239 version = strings.TrimPrefix(parts[2], "v") 240 break 241 } 242 // 2. [github.com/fastly/compute-sdk-go] v0.1.7 243 if parts[0] == moduleName { 244 version = strings.TrimPrefix(parts[1], "v") 245 break 246 } 247 } 248 } 249 250 if err := scanner.Err(); err != nil { 251 return configConstraint 252 } 253 254 if version == "" { 255 return configConstraint 256 } 257 258 gomodVersion, err := semver.NewVersion(version) 259 if err != nil { 260 return configConstraint 261 } 262 263 // 0.2.0 introduces the break by bumping the TinyGo minimum version to 0.28.1 264 breakingSDKVersion, err := semver.NewVersion("0.2.0") 265 if err != nil { 266 return configConstraint 267 } 268 269 if gomodVersion.LessThan(breakingSDKVersion) { 270 return fallback 271 } 272 273 return configConstraint 274 } 275 276 // toolchainConstraint warns the user if the required constraint is not met. 277 // 278 // NOTE: We don't stop the build as their toolchain may compile successfully. 279 // The warning is to help a user know something isn't quite right and gives them 280 // the opportunity to do something about it if they choose. 281 func (g *Go) toolchainConstraint(toolchain, pattern, constraint string) { 282 if g.verbose { 283 text.Info(g.output, "The Fastly CLI build step requires a %s version '%s'.\n\n", toolchain, constraint) 284 } 285 286 versionCommand := fmt.Sprintf("%s version", toolchain) 287 args := strings.Split(versionCommand, " ") 288 289 // gosec flagged this: 290 // G204 (CWE-78): Subprocess launched with function call as argument or cmd arguments 291 // Disabling as we trust the source of the variable. 292 // #nosec 293 // nosemgrep 294 cmd := exec.Command(args[0], args[1:]...) 295 stdoutStderr, err := cmd.CombinedOutput() 296 output := string(stdoutStderr) 297 if err != nil { 298 return 299 } 300 301 versionPattern := regexp.MustCompile(pattern) 302 match := versionPattern.FindStringSubmatch(output) 303 if len(match) < 2 { // We expect a pattern with one capture group. 304 return 305 } 306 version := match[1] 307 308 v, err := semver.NewVersion(version) 309 if err != nil { 310 return 311 } 312 313 c, err := semver.NewConstraint(constraint) 314 if err != nil { 315 return 316 } 317 318 if !c.Check(v) { 319 text.Warning(g.output, "The %s version '%s' didn't meet the constraint '%s'\n\n", toolchain, version, constraint) 320 } 321 }