cmd/go/build/main.go (118 lines of code) (raw):

// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Implements go/build buildpack. // The build buildpack runs go build. package main import ( "fmt" "os" "path/filepath" "strings" "github.com/GoogleCloudPlatform/buildpacks/pkg/devmode" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/GoogleCloudPlatform/buildpacks/pkg/golang" ) const ( noGoFileError = "no Go files in" cannotFindModuleError = "cannot find module" ) func main() { gcp.Main(detectFn, buildFn) } func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) { atLeastOne, err := ctx.HasAtLeastOne("*.go") if err != nil { return nil, fmt.Errorf("finding *.go files: %w", err) } if !atLeastOne { return gcp.OptOut("no .go files found"), nil } return gcp.OptIn("found .go files"), nil } func buildFn(ctx *gcp.Context) error { // Keep GOCACHE in Devmode for faster rebuilds. cl, err := ctx.Layer("gocache", gcp.BuildLayer, gcp.LaunchLayerIfDevMode) if err != nil { return fmt.Errorf("creating layer: %w", err) } if devmode.Enabled(ctx) { cl.LaunchEnvironment.Override("GOCACHE", cl.Path) } // Create a layer for the compiled binary. Add it to PATH in case // users wish to invoke the binary manually. bl, err := ctx.Layer("bin", gcp.LaunchLayer) if err != nil { return fmt.Errorf("creating layer: %w", err) } bl.LaunchEnvironment.Prepend("PATH", string(os.PathListSeparator), bl.Path) outBin := filepath.Join(bl.Path, golang.OutBin) buildable, err := goBuildable(ctx) if err != nil { return fmt.Errorf("unable to find a valid buildable: %w", err) } // Build the application. bld := []string{"go", "build"} bld = append(bld, goBuildFlags()...) bld = append(bld, "-o", outBin) bld = append(bld, buildable) // BuildDirEnv should only be set by App Engine buildpacks. workdir := os.Getenv(golang.BuildDirEnv) if workdir == "" { workdir = ctx.ApplicationRoot() } if _, err := ctx.Exec(bld, gcp.WithEnv("GOCACHE="+cl.Path), gcp.WithWorkDir(workdir), gcp.WithMessageProducer(printTipsAndKeepStderrTail(ctx)), gcp.WithUserAttribution); err != nil { return err } // Configure the entrypoint for production. Use the full path to save `skaffold debug` // from fetching the remote container image (tens to hundreds of megabytes), which is slow. if !devmode.Enabled(ctx) { ctx.AddWebProcess([]string{outBin}) return nil } // Configure the entrypoint and metadata for dev mode. if err := devmode.AddFileWatcherProcess(ctx, devmode.Config{ BuildCmd: bld, RunCmd: []string{outBin}, Ext: devmode.GoWatchedExtensions, }); err != nil { return fmt.Errorf("adding devmode file watcher: %w", err) } return nil } func goBuildable(ctx *gcp.Context) (string, error) { // The user tells us what to build. if buildable, ok := os.LookupEnv(env.Buildable); ok { return buildable, nil } // We have to guess which package/file to build. // `go build` will by default build the `.` package // but we try to be smarter by searching for a valid buildable. buildables, err := searchBuildables(ctx) if err != nil { return "", err } if len(buildables) == 1 { return buildables[0], nil } // Found no buildable or multiple buildables. Let Go build the default package. return ".", nil } // searchBuildables searches the source for all the files that contain // a `main()` entrypoint. func searchBuildables(ctx *gcp.Context) ([]string, error) { result, err := ctx.Exec([]string{"go", "list", "-f", `{{if eq .Name "main"}}{{.Dir}}{{end}}`, "./..."}, gcp.WithUserAttribution) if err != nil { return nil, err } var buildables []string for _, dir := range strings.Fields(result.Stdout) { rel, err := filepath.Rel(ctx.ApplicationRoot(), dir) if err != nil { return nil, fmt.Errorf("unable to find relative path for %q: %w", dir, err) } buildables = append(buildables, "./"+rel) } return buildables, nil } func goBuildFlags() []string { var flags []string if v := os.Getenv(env.GoGCFlags); v != "" { flags = append(flags, "-gcflags", v) } if v := os.Getenv(env.GoLDFlags); v != "" { flags = append(flags, "-ldflags", v) } return flags } func printTipsAndKeepStderrTail(ctx *gcp.Context) gcp.MessageProducer { return func(result *gcp.ExecResult) string { if result.ExitCode != 0 { // If `go build` fails with any of those two errors, there's a great chance // that we are not building the right package. if strings.Contains(result.Stderr, noGoFileError) || strings.Contains(result.Stderr, cannotFindModuleError) { ctx.Tipf("Tip: %q env var configures which Go package is built. Default is '.'", env.Buildable) } } return gcp.KeepStderrTail(result) } }