github.com/mweagle/Sparta@v1.15.0/system/goversion.go (about) 1 package system 2 3 import ( 4 "flag" 5 "fmt" 6 "go/parser" 7 "go/token" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 "strings" 14 15 "github.com/pkg/errors" 16 "github.com/sirupsen/logrus" 17 ) 18 19 func ensureMainEntrypoint(logger *logrus.Logger) error { 20 // Don't do this for "go test" runs 21 if flag.Lookup("test.v") != nil { 22 logger.Debug("Skipping main() check for test") 23 return nil 24 } 25 26 fset := token.NewFileSet() 27 packageMap, parseErr := parser.ParseDir(fset, ".", nil, parser.PackageClauseOnly) 28 if parseErr != nil { 29 return errors.Errorf("Failed to parse source input: %s", parseErr.Error()) 30 } 31 logger.WithFields(logrus.Fields{ 32 "SourcePackages": packageMap, 33 }).Debug("Checking working directory") 34 35 // If there isn't a main defined, we're in the wrong directory.. 36 mainPackageCount := 0 37 for eachPackage := range packageMap { 38 if eachPackage == "main" { 39 mainPackageCount++ 40 } 41 } 42 if mainPackageCount <= 0 { 43 unlikelyBinaryErr := fmt.Errorf("error: It appears your application's `func main() {}` is not in the current working directory. Please run this command in the same directory as `func main() {}`") 44 return unlikelyBinaryErr 45 } 46 return nil 47 } 48 49 // GoVersion returns the configured go version for this system 50 func GoVersion(logger *logrus.Logger) (string, error) { 51 runtimeVersion := runtime.Version() 52 // Get the golang version from the output: 53 // Matts-MBP:Sparta mweagle$ go version 54 // go version go1.8.1 darwin/amd64 55 golangVersionRE := regexp.MustCompile(`go(\d+\.\d+(\.\d+)?)`) 56 matches := golangVersionRE.FindStringSubmatch(runtimeVersion) 57 if len(matches) > 2 { 58 return matches[1], nil 59 } 60 logger.WithFields(logrus.Fields{ 61 "Output": runtimeVersion, 62 }).Warn("Unable to find Golang version using RegExp - using current version") 63 return runtimeVersion, nil 64 } 65 66 // GoPath returns either $GOPATH or the new $HOME/go path 67 // introduced with Go 1.8 68 func GoPath() string { 69 gopath := os.Getenv("GOPATH") 70 if gopath == "" { 71 home := os.Getenv("HOME") 72 gopath = filepath.Join(home, "go") 73 } 74 return gopath 75 } 76 77 // BuildGoBinary is a helper to build a go binary with the given options 78 func BuildGoBinary(serviceName string, 79 executableOutput string, 80 useCGO bool, 81 buildID string, 82 userSuppliedBuildTags string, 83 linkFlags string, 84 noop bool, 85 logger *logrus.Logger) error { 86 87 // Before we do anything, let's make sure there's a `main` package in this directory. 88 ensureMainPackageErr := ensureMainEntrypoint(logger) 89 if ensureMainPackageErr != nil { 90 return ensureMainPackageErr 91 } 92 // Go generate 93 cmd := exec.Command("go", "generate") 94 if logger.Level == logrus.DebugLevel { 95 cmd = exec.Command("go", "generate", "-v", "-x") 96 } 97 cmd.Env = os.Environ() 98 commandString := fmt.Sprintf("%s", cmd.Args) 99 logger.Info(fmt.Sprintf("Running `%s`", strings.Trim(commandString, "[]"))) 100 goGenerateErr := RunOSCommand(cmd, logger) 101 if nil != goGenerateErr { 102 return goGenerateErr 103 } 104 // TODO: Smaller binaries via linker flags 105 // Ref: https://blog.filippo.io/shrink-your-go-binaries-with-this-one-weird-trick/ 106 noopTag := "" 107 if noop { 108 noopTag = "noop " 109 } 110 111 buildTags := []string{ 112 "lambdabinary", 113 "linux", 114 } 115 if noopTag != "" { 116 buildTags = append(buildTags, noopTag) 117 } 118 if userSuppliedBuildTags != "" { 119 userBuildTagsParts := strings.Split(userSuppliedBuildTags, " ") 120 buildTags = append(buildTags, userBuildTagsParts...) 121 } 122 userBuildFlags := []string{"-tags", strings.Join(buildTags, " ")} 123 124 // Append all the linker flags 125 // Stamp the service name into the binary 126 // We need to stamp the servicename into the aws binary so that if the user 127 // chose some type of dynamic stack name at provision time, the name 128 // we use at execution time has that value. This is necessary because 129 // the function dispatch logic uses the AWS_LAMBDA_FUNCTION_NAME environment 130 // variable to do the lookup. And in effect, this value has to be unique 131 // across an account, since functions cannot have the same name 132 // Custom flags for the binary 133 linkerFlags := map[string]string{ 134 "StampedServiceName": serviceName, 135 "StampedBuildID": buildID, 136 } 137 for eachFlag, eachValue := range linkerFlags { 138 linkFlags = fmt.Sprintf("%s -s -w -X github.com/mweagle/Sparta.%s=%s", 139 linkFlags, 140 eachFlag, 141 eachValue) 142 } 143 linkFlags = strings.TrimSpace(linkFlags) 144 if len(linkFlags) != 0 { 145 userBuildFlags = append(userBuildFlags, "-ldflags", linkFlags) 146 } 147 // If this is CGO, do the Docker build if we're doing an actual 148 // provision. Otherwise use the "normal" build to keep things 149 // a bit faster. 150 var cmdError error 151 if useCGO { 152 currentDir, currentDirErr := os.Getwd() 153 if nil != currentDirErr { 154 return currentDirErr 155 } 156 gopathVersion, gopathVersionErr := GoVersion(logger) 157 if nil != gopathVersionErr { 158 return gopathVersionErr 159 } 160 161 gopath := GoPath() 162 containerGoPath := "/usr/src/gopath" 163 // Get the package path in the current directory 164 // so that we can it to the container path 165 packagePath := strings.TrimPrefix(currentDir, gopath) 166 volumeMountMapping := fmt.Sprintf("%s:%s", gopath, containerGoPath) 167 containerSourcePath := fmt.Sprintf("%s%s", containerGoPath, packagePath) 168 169 // If there's one from the environment, use that... 170 // TODO 171 172 // Otherwise, make one... 173 174 // Any CGO paths? 175 cgoLibPath := fmt.Sprintf("%s/cgo/lib", containerSourcePath) 176 cgoIncludePath := fmt.Sprintf("%s/cgo/include", containerSourcePath) 177 178 // Pass any SPARTA_* prefixed environment variables to the docker build 179 // 180 goosTarget := os.Getenv("SPARTA_GOOS") 181 if goosTarget == "" { 182 goosTarget = "linux" 183 } 184 goArch := os.Getenv("SPARTA_GOARCH") 185 if goArch == "" { 186 goArch = "amd64" 187 } 188 spartaEnvVars := []string{ 189 // "-e", 190 // fmt.Sprintf("GOPATH=%s", containerGoPath), 191 "-e", 192 fmt.Sprintf("GOOS=%s", goosTarget), 193 "-e", 194 fmt.Sprintf("GOARCH=%s", goArch), 195 "-e", 196 fmt.Sprintf("CGO_LDFLAGS=-L%s", cgoLibPath), 197 "-e", 198 fmt.Sprintf("CGO_CFLAGS=-I%s", cgoIncludePath), 199 } 200 // User vars 201 for _, eachPair := range os.Environ() { 202 if strings.HasPrefix(eachPair, "SPARTA_") { 203 spartaEnvVars = append(spartaEnvVars, "-e", eachPair) 204 } 205 } 206 dockerBuildArgs := []string{ 207 "run", 208 "--rm", 209 "-v", 210 volumeMountMapping, 211 "-w", 212 containerSourcePath} 213 dockerBuildArgs = append(dockerBuildArgs, spartaEnvVars...) 214 dockerBuildArgs = append(dockerBuildArgs, 215 fmt.Sprintf("golang:%s", gopathVersion), 216 "go", 217 "build", 218 "-o", 219 executableOutput, 220 "-buildmode=default", 221 ) 222 dockerBuildArgs = append(dockerBuildArgs, userBuildFlags...) 223 cmd = exec.Command("docker", dockerBuildArgs...) 224 cmd.Env = os.Environ() 225 logger.WithFields(logrus.Fields{ 226 "Name": executableOutput, 227 "Args": dockerBuildArgs, 228 }).Info("Building `cgo` library in Docker") 229 cmdError = RunOSCommand(cmd, logger) 230 231 // If this succeeded, let's find the .h file and move it into the scratch 232 // Try to keep things tidy... 233 if nil == cmdError { 234 soExtension := filepath.Ext(executableOutput) 235 headerFilepath := fmt.Sprintf("%s.h", strings.TrimSuffix(executableOutput, soExtension)) 236 _, headerFileErr := os.Stat(headerFilepath) 237 if nil == headerFileErr { 238 targetPath, targetPathErr := TemporaryFile(".sparta", filepath.Base(headerFilepath)) 239 if nil != targetPathErr { 240 headerFileErr = targetPathErr 241 } else { 242 headerFileErr = os.Rename(headerFilepath, targetPath.Name()) 243 } 244 } 245 if nil != headerFileErr { 246 logger.WithFields(logrus.Fields{ 247 "Path": headerFilepath, 248 }).Warn("Failed to move .h file to scratch directory") 249 } 250 } 251 } else { 252 // Build the regular version 253 buildArgs := []string{ 254 "build", 255 "-o", 256 executableOutput, 257 } 258 // Debug flags? 259 if logger.Level == logrus.DebugLevel { 260 buildArgs = append(buildArgs, "-v") 261 } 262 buildArgs = append(buildArgs, userBuildFlags...) 263 buildArgs = append(buildArgs, ".") 264 cmd = exec.Command("go", buildArgs...) 265 cmd.Env = os.Environ() 266 cmd.Env = append(cmd.Env, "GOOS=linux", "GOARCH=amd64") 267 logger.WithFields(logrus.Fields{ 268 "Name": executableOutput, 269 }).Info("Compiling binary") 270 cmdError = RunOSCommand(cmd, logger) 271 } 272 return cmdError 273 } 274 275 // TemporaryFile creates a stable temporary filename in the current working 276 // directory 277 func TemporaryFile(scratchDir string, name string) (*os.File, error) { 278 workingDir, err := os.Getwd() 279 if nil != err { 280 return nil, err 281 } 282 283 // Use a stable temporary name 284 temporaryPath := filepath.Join(workingDir, scratchDir, name) 285 buildDir := filepath.Dir(temporaryPath) 286 mkdirErr := os.MkdirAll(buildDir, os.ModePerm) 287 if nil != mkdirErr { 288 return nil, mkdirErr 289 } 290 291 tmpFile, err := os.Create(temporaryPath) 292 if err != nil { 293 return nil, errors.New("Failed to create temporary file: " + err.Error()) 294 } 295 296 return tmpFile, nil 297 }