pkg/golang/golang.go (262 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.
// Package golang contains Go buildpack library code.
package golang
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/GoogleCloudPlatform/buildpacks/pkg/appengine"
"github.com/GoogleCloudPlatform/buildpacks/pkg/cache"
"github.com/GoogleCloudPlatform/buildpacks/pkg/env"
"github.com/GoogleCloudPlatform/buildpacks/pkg/fetch"
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
"github.com/GoogleCloudPlatform/buildpacks/pkg/version"
"github.com/buildpacks/libcnb/v2"
"github.com/Masterminds/semver"
)
const (
// OutBin is the name of the final compiled binary produced by Go buildpacks.
OutBin = "main"
// BuildDirEnv is an environment variable that buildpacks can use to communicate the working directory to `go build`.
BuildDirEnv = "GOOGLE_INTERNAL_BUILD_DIR"
// The name of the layer where the GOPATH is stored
goPathLayerName = "gopath"
// The key used when a layers' cache is keyed off of the go mod
goModCacheKey = "go-mod-sha"
envGoVersion = "GOOGLE_GO_VERSION"
)
var (
// goVersionRegexp is used to parse `go version`'s output.
goVersionRegexp = regexp.MustCompile(`^go version go(\d+(\.\d+){1,2})([a-z]+\d+)? .*$`)
// goModVersionRegexp is used to get correct declaration of Go version from go.mod file.
goModVersionRegexp = regexp.MustCompile(`(?m)^\s*go\s+(\d+(\.\d+){1,2})\s*$`)
// goVersionsURL can be use to download a list of available, stable versions of Go.
goVersionsURL = "https://go.dev/dl/?mode=json"
)
// goRelease represents an entry on the go.dev downloads page.
type goRelease struct {
Version string `json:"version"`
Stable bool `json:"stable"`
}
// SupportsAppEngineApis is a Go buildpack specific function that returns true if App Engine API access is enabled
func SupportsAppEngineApis(ctx *gcp.Context) (bool, error) {
if IsGo111Runtime() {
return true, nil
}
return appengine.ApisEnabled(ctx)
}
// SupportsAutoVendor returns true if the Go version supports automatic detection of the vendor directory.
// This feature is supported by Go 1.14 and higher.
func SupportsAutoVendor(ctx *gcp.Context) (bool, error) {
return VersionMatches(ctx, ">=1.14.0")
}
// SupportsGoProxyFallback returns true if the Go version supports fallback in GOPROXY using the pipe character.
// This feature is supported by Go 1.15 and higher.
func SupportsGoProxyFallback(ctx *gcp.Context) (bool, error) {
return VersionMatches(ctx, ">=1.15.0")
}
// SupportsGoCleanModCache returns true if the Go version supports `go clean -modcache` without loading the packages.
// The command fails if the packages aren't available for Go 1.12 and lower.
// The feature to skip loading the packages is only supported by Go 1.13 and higher.
// More information can be found at golang.org/issue/28680 and golang.org/issue/28459.
func SupportsGoCleanModCache(ctx *gcp.Context) (bool, error) {
return VersionMatches(ctx, ">=1.13.0")
}
// SupportsGoGet returns true if the Go version supports `go get`.
// For versions above 1.22.0+ `go get` is not supported outside of modules in legacy gopath mode.
func SupportsGoGet(ctx *gcp.Context) (bool, error) {
v, err := RuntimeVersion(ctx)
if err != nil {
return false, err
}
if v == "" {
return false, nil
}
return VersionMatches(ctx, "<1.22.0", v)
}
// SupportsVendorModificaton returns true if the Go version supports modifying vendor directory without modifying vendor/modules.txt.
// Versions 1.23.0 and later require vendored packages to be present in vendor/modules.txt to be imported.
func SupportsVendorModificaton(ctx *gcp.Context) (bool, error) {
v, _ := RuntimeVersion(ctx)
// if runtimeVersion is not set, it uses latest version (which is going to be >=1.23.0) which does not support vendor modification without modifying vendor/modules.txt.
if v == "" {
return false, nil
}
// if runtimeVersion is set, check if it is <1.23.0.
version, err := semver.NewVersion(v)
if err != nil {
return false, gcp.InternalErrorf("unable to parse version string %q: %w", v, err)
}
goVersionMatches, err := semver.NewConstraint("<1.23.0")
if err != nil {
return false, gcp.InternalErrorf("unable to parse version range %q: %w", v, err)
}
if goVersionMatches.Check(version) {
return true, nil
}
return false, nil
}
// VersionMatches checks if the installed version of Go and the version specified in go.mod match the given version range.
// The range string has the following format: https://github.com/blang/semver#ranges.
func VersionMatches(ctx *gcp.Context, versionRange string, goVersions ...string) (bool, error) {
var v string
var err error
if len(goVersions) == 0 {
v, err = GoModVersion(ctx)
if err != nil {
return false, err
}
} else {
v = goVersions[0]
}
if v == "" {
return false, nil
}
version, err := semver.NewVersion(v)
if err != nil {
return false, gcp.InternalErrorf("unable to parse go.mod version string %q: %s", v, err)
}
goVersionMatches, err := semver.NewConstraint(versionRange)
if err != nil {
return false, gcp.InternalErrorf("unable to parse version range %q: %s", v, err)
}
if !goVersionMatches.Check(version) {
return false, nil
}
v, err = GoVersion(ctx)
if err != nil {
return false, err
}
version, err = semver.NewVersion(v)
if err != nil {
return false, gcp.InternalErrorf("unable to parse Go version string %q: %s", v, err)
}
return goVersionMatches.Check(version), nil
}
// GoVersion reads the version of the installed Go runtime.
func GoVersion(ctx *gcp.Context) (string, error) {
v, err := readGoVersion(ctx)
if err != nil {
return "", err
}
match := goVersionRegexp.FindStringSubmatch(v)
if len(match) < 2 || match[1] == "" {
return "", gcp.InternalErrorf("unable to find go version in %q", v)
}
return match[1], nil
}
// GoModVersion reads the version of Go from a go.mod file if present.
// If not present or if version isn't there returns an empty string.
func GoModVersion(ctx *gcp.Context) (string, error) {
v, err := readGoMod(ctx)
if err != nil {
return "", fmt.Errorf("reading go.mod: %w", err)
}
if v == "" {
return v, nil
}
match := goModVersionRegexp.FindStringSubmatch(v)
if len(match) < 2 || match[1] == "" {
return "", nil
}
return match[1], nil
}
// readGoVersion returns the output of `go version`.
// It can be overridden for testing.
var readGoVersion = func(ctx *gcp.Context) (string, error) {
result, err := ctx.Exec([]string{"go", "version"})
if err != nil {
return "", err
}
return result.Stdout, nil
}
// cleanModCache deletes the downloaded cached dependencies using `go clean -modcache`.
// The cached dependencies are written without write access and attempt
// to clear layer using ctx.ClearLayer(l) fails with permission denied errors.
// It can be overridden for testing.
var cleanModCache = func(ctx *gcp.Context) error {
_, err := ctx.Exec([]string{"go", "clean", "-modcache"})
return err
}
// readGoMod reads the go.mod file if present. If not present, returns an empty string.
// It can be overridden for testing.
var readGoMod = func(ctx *gcp.Context) (string, error) {
goModPath := goModPath(ctx)
goModExists, err := ctx.FileExists(goModPath)
if err != nil {
return "", err
}
if !goModExists {
return "", nil
}
bytes, err := ctx.ReadFile(goModPath)
if err != nil {
return "", err
}
return string(bytes), nil
}
// NewGoWorkspaceLayer returns a new layer for `go env GOPATH` or the go workspace. The
// layer is configured for caching if possible. It only supports caching for "go mod"
// based builds.
func NewGoWorkspaceLayer(ctx *gcp.Context) (*libcnb.Layer, error) {
l, err := ctx.Layer(goPathLayerName, gcp.BuildLayer, gcp.CacheLayer, gcp.LaunchLayerIfDevMode)
if err != nil {
return nil, fmt.Errorf("creating %v layer: %w", goPathLayerName, err)
}
l.BuildEnvironment.Override("GOPATH", l.Path)
l.BuildEnvironment.Override("GO111MODULE", "on")
// Set GOPROXY to ensure no additional dependency is downloaded at built time.
// All of them are downloaded here.
l.BuildEnvironment.Override("GOPROXY", "off")
shouldEnablePkgCache, err := SupportsGoCleanModCache(ctx)
if err != nil {
return nil, fmt.Errorf("checking for go pkg cache support: %w", err)
}
if !shouldEnablePkgCache {
l.Cache = false
return l, nil
}
hash, cached, err := cache.HashAndCheck(ctx, l, goModCacheKey, cache.WithFiles(goModPath(ctx)))
if err != nil {
if os.IsNotExist(err) {
// when go.mod doesn't exist, clear any previously cached bits and return an empty layer
l.Cache = false
cleanModCache(ctx)
return l, nil
}
return nil, err
}
if cached {
return l, nil
}
ctx.Debugf("go.mod SHA has changed: clearing GOPATH layer's cache")
cleanModCache(ctx)
cache.Add(ctx, l, goModCacheKey, hash)
return l, nil
}
func goModPath(ctx *gcp.Context) string {
return filepath.Join(ctx.ApplicationRoot(), "go.mod")
}
// ExecWithGoproxyFallback runs the given command with a GOPROXY fallback.
// Before Go 1.14, Go would fall back to direct only if a 404 or 410 error ocurred, for those
// versions, we explictly disable GOPROXY and try again on any error.
// For newer versions of Go, we take advantage of the "pipe" character which has the same effect.
func ExecWithGoproxyFallback(ctx *gcp.Context, cmd []string, opts ...gcp.ExecOption) (*gcp.ExecResult, error) {
supportsGoProxy, err := SupportsGoProxyFallback(ctx)
if err != nil {
return nil, fmt.Errorf("checking for go proxy support: %w", err)
}
if supportsGoProxy {
opts = append(opts, gcp.WithEnv("GOPROXY=https://proxy.golang.org|direct"))
return ctx.Exec(cmd, opts...)
}
result, err := ctx.Exec(cmd, opts...)
if err == nil {
return result, nil
}
ctx.Warnf("%q failed. Retrying with GOSUMDB=off GOPROXY=direct. Error: %v", strings.Join(cmd, " "), err)
opts = append(opts, gcp.WithEnv("GOSUMDB=off", "GOPROXY=direct"))
return ctx.Exec(cmd, opts...)
}
// IsGo111Runtime returns true when the GOOGLE_RUNTIME is go111. This will be
// true when using GCF or GAE with go 1.11.
func IsGo111Runtime() bool {
return os.Getenv(env.Runtime) == "go111"
}
// RuntimeVersion returns the runtime version for the go app.
func RuntimeVersion(ctx *gcp.Context) (string, error) {
if version := os.Getenv(envGoVersion); version != "" {
ctx.Logf("Using runtime version from %s: %s", envGoVersion, version)
return version, nil
}
if version := os.Getenv(env.RuntimeVersion); version != "" {
ctx.Logf("Using runtime version from %s: %s", env.RuntimeVersion, version)
return version, nil
}
ctx.Logf("Using latest stable Go version")
return "", nil
}
// ResolveGoVersion finds the latest version of Go that matches the provided semver constraint.
func ResolveGoVersion(verConstraint string) (string, error) {
if isSupportedUnstableGoVersion(verConstraint) || isExactGoSemver(verConstraint) {
return verConstraint, nil
}
var releases []goRelease
if err := fetch.JSON(goVersionsURL, &releases); err != nil {
return "", gcp.InternalErrorf("fetching Go releases: %v", err)
}
var versions []string
for _, r := range releases {
if r.Stable {
versions = append(versions, strings.TrimPrefix(r.Version, "go"))
}
}
v, err := version.ResolveVersion(verConstraint, versions, version.WithoutSanitization)
if err != nil {
return "", gcp.UserErrorf("invalid Go version specified: %v, You can refer to %s for a list of stable Go releases.", goVersionsURL, err)
}
return v, nil
}
// When launching a new runtime, we need to test with RC candidate which will eventually be replaced
// by a stable candidate. Till then, we will support these unstable releases in the QA for testing.
func isSupportedUnstableGoVersion(constraint string) bool {
if strings.Count(constraint, ".") == 1 && strings.Count(constraint, "rc") == 1 {
return true
}
return false
}
// isExactGoSemver returns true if a given string is a precisely specified go version. That is, it
// is not a version constraint. The logic for this is unique for Go because new major releases do
// not include a trailing zero (e.g. go1.20).
func isExactGoSemver(constraint string) bool {
if c := strings.Count(constraint, "."); c != 1 && c != 2 {
// The constraint must include the major, minor, and patch segments to be exact. By default,
// semver.NewVersion will set these to zero so we must validate this separately.
return false
}
_, err := semver.NewVersion(constraint)
return err == nil
}