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  }