pkg/ruby/ruby.go (136 lines of code) (raw):

// Copyright 2022 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 ruby contains Ruby buildpack library code. package ruby import ( "fmt" "os" "path/filepath" "strings" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" "github.com/Masterminds/semver" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" ) const defaultVersion = "3.3.*" // RubyVersionKey is the environment variable name used to store the Ruby version installed. const RubyVersionKey = "build_ruby_version" // DetectVersion detects ruby version from the environment, Gemfile.lock, gems.locked, or falls // back to a default version. func DetectVersion(ctx *gcp.Context) (string, error) { versionFromEnv := os.Getenv(env.RuntimeVersion) // The two lock files have the same format for Ruby version lockFiles := []string{"Gemfile.lock", "gems.locked"} // If environment is GAE or GCF, skip lock file validation. // App Engine specific validation is done in a different buildpack. if env.IsGAE() || env.IsGCF() { if versionFromEnv != "" { ctx.Logf( "Using runtime version from environment variable %s: %s", env.RuntimeVersion, versionFromEnv) return versionFromEnv, nil } } versionFromRubyVersion, err := getVersionFromRubyVersion(ctx) if err != nil { return "", err } if versionFromEnv != "" && versionFromRubyVersion != "" && versionFromRubyVersion != versionFromEnv { return "", gcp.UserErrorf( "There is a conflict between Ruby versions specified in .ruby-version file and the %s environment variable. "+ "Please resolve the conflict by choosing only one way to specify the ruby version.", env.RuntimeVersion) } for _, lockFileName := range lockFiles { path := filepath.Join(ctx.ApplicationRoot(), lockFileName) pathExists, err := ctx.FileExists(path) if err != nil { return "", err } if pathExists { lockedVersion, err := ParseRubyVersion(path) if err != nil { return "", gcp.UserErrorf("Error %q in: %s", err, lockFileName) } // Lockfile doesn't contain a ruby version, so we can move on if lockedVersion == "" { break } // Bundler doesn't allow us to override a version of ruby if it's locked in the lock file // The env will still be useful if a project doesn't lock ruby version or doesn't use bundler if versionFromEnv != "" && lockedVersion != versionFromEnv { return "", gcp.UserErrorf( "Ruby version %q in %s can't be overriden to %q using %s environment variable", lockedVersion, lockFileName, versionFromEnv, env.RuntimeVersion) } if versionFromRubyVersion != "" && lockedVersion != versionFromRubyVersion { return "", gcp.UserErrorf( "There is a conflict between the Ruby version %q in %s and %q in .ruby-version file."+ "Please resolve the conflict by choosing only one way to specify the ruby version.", lockedVersion, lockFileName, versionFromRubyVersion) } return lockedVersion, err } } if versionFromEnv != "" { ctx.Logf( "Using runtime version from environment variable %s: %s", env.RuntimeVersion, versionFromEnv) return versionFromEnv, nil } if versionFromRubyVersion != "" { ctx.Logf( "Using runtime version from .ruby-version file: %s", versionFromRubyVersion) return versionFromRubyVersion, nil } return defaultVersion, nil } // IsRuby25 returns true if the build environment has Ruby 2.5.x installed. func IsRuby25(ctx *gcp.Context) bool { return strings.HasPrefix(os.Getenv(RubyVersionKey), "2.5") } // SupportsBundler1 returns true if the installed Ruby version is compatible with Bundler 1. // Bundler 1 breaks with Ruby 3.2. This functions returns true for all versions older than 3.2. func SupportsBundler1(ctx *gcp.Context) (bool, error) { rubyVersion, err := semver.NewVersion(os.Getenv(RubyVersionKey)) if err != nil { return false, err } ruby32Version, _ := semver.NewVersion("3.2.0") return rubyVersion.LessThan(ruby32Version), nil } // NeedsRailsAssetPrecompile detects if asset precompilation is required in a Ruby on Rails app. func NeedsRailsAssetPrecompile(ctx *gcp.Context) (bool, error) { isRailsApp, err := ctx.FileExists("bin", "rails") if err != nil { return false, fmt.Errorf("finding bin/rails: %w", err) } if !isRailsApp { return false, nil } assetsExists, err := ctx.FileExists("app", "assets") if err != nil { return false, err } if !assetsExists { return false, nil } manifestExists, err := ctx.FileExists("public", "assets", "manifest.yml") if err != nil { return false, err } if manifestExists { return false, nil } matches, err := ctx.Glob("public/assets/manifest-*.json") if err != nil { return false, fmt.Errorf("finding manifets: %w", err) } if matches != nil { return false, nil } matches, err = ctx.Glob("public/assets/.sprockets-manifest-*.json") if err != nil { return false, fmt.Errorf("finding sprockets-manifets: %w", err) } if matches != nil { return false, nil } return true, nil } // Function to get the ruby version from .ruby-version file. func getVersionFromRubyVersion(ctx *gcp.Context) (string, error) { path := filepath.Join(ctx.ApplicationRoot(), ".ruby-version") pathExists, err := ctx.FileExists(path) if err != nil { return "", err } if pathExists { version, err := os.ReadFile(path) if err != nil { return "", gcp.UserErrorf("Error %q in: %s", err, ".ruby-version") } return string(version), nil } return "", nil }