cmd/ruby/bundle/main.go (157 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 ruby/bundle buildpack. // The bundle buildpack installs dependencies using bundle. package main import ( "fmt" "path/filepath" "github.com/GoogleCloudPlatform/buildpacks/pkg/buildererror" "github.com/GoogleCloudPlatform/buildpacks/pkg/cache" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/buildpacks/libcnb/v2" ) const ( layerName = "gems" dependencyHashKey = "dependency_hash" rubyVersionKey = "ruby_version" ) func main() { gcp.Main(detectFn, buildFn) } func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) { gemfileExists, err := ctx.FileExists("Gemfile") if err != nil { return nil, err } if gemfileExists { return gcp.OptInFileFound("Gemfile"), nil } gemsRbExists, err := ctx.FileExists("gems.rb") if err != nil { return nil, err } if gemsRbExists { return gcp.OptInFileFound("gems.rb"), nil } return gcp.OptOut("no Gemfile or gems.rb found"), nil } func buildFn(ctx *gcp.Context) error { var lockFile string hasGemfile, err := ctx.FileExists("Gemfile") if err != nil { return err } hasGemsRB, err := ctx.FileExists("gems.rb") if err != nil { return err } if hasGemfile { if hasGemsRB { ctx.Warnf("Gemfile and gems.rb both exist. Using Gemfile.") } gemfileLockExists, err := ctx.FileExists("Gemfile.lock") if err != nil { return err } if !gemfileLockExists { return buildererror.Errorf(buildererror.StatusFailedPrecondition, "Could not find Gemfile.lock file in your app. Please make sure your bundle is up to date before deploying.") } lockFile = "Gemfile.lock" } else if hasGemsRB { gemsLockedExists, err := ctx.FileExists("gems.locked") if err != nil { return err } if !gemsLockedExists { return buildererror.Errorf(buildererror.StatusFailedPrecondition, "Could not find gems.locked file in your app. Please make sure your bundle is up to date before deploying.") } lockFile = "gems.locked" } // Remove any user-provided local bundle config and cache that can interfere with the build process. if err := ctx.RemoveAll(".bundle"); err != nil { return err } deps, err := ctx.Layer(layerName, gcp.BuildLayer, gcp.CacheLayer, gcp.LaunchLayer) if err != nil { return fmt.Errorf("creating %v layer: %w", layerName, err) } // This layer directory contains the files installed by bundler into the application .bundle directory bundleOutput := filepath.Join(deps.Path, ".bundle") cached, err := checkCache(ctx, deps, cache.WithFiles(lockFile)) if err != nil { return fmt.Errorf("checking cache: %w", err) } localGemsDir := filepath.Join(".bundle", "gems") localBinDir := filepath.Join(".bundle", "bin") // Ensure the GCP runtime platform is present in the lockfile. This is needed for Bundler >= 2.2, in case the user's lockfile is specific to a different platform. if _, err := ctx.Exec([]string{"bundle", "config", "--local", "without", "development test"}, gcp.WithUserAttribution); err != nil { return err } if _, err := ctx.Exec([]string{"bundle", "config", "--local", "path", localGemsDir}, gcp.WithUserAttribution); err != nil { return err } // This line will override user provided BUNDLED WITH in the Gemfile.lock // It'll use the currently activated bundler version instead // This was a change in bundler 2.1+ // https://github.com/rubygems/rubygems/issues/5683 if _, err := ctx.Exec([]string{"bundle", "lock", "--add-platform", "x86_64-linux"}, gcp.WithUserAttribution); err != nil { return err } if _, err := ctx.Exec([]string{"bundle", "lock", "--add-platform", "ruby"}, gcp.WithUserAttribution); err != nil { return err } if err := ctx.RemoveAll(".bundle"); err != nil { return err } if cached { ctx.CacheHit(layerName) } else { ctx.CacheMiss(layerName) // Install the bundle locally into .bundle/gems if _, err := ctx.Exec([]string{"bundle", "config", "--local", "deployment", "true"}, gcp.WithUserAttribution); err != nil { return err } if _, err := ctx.Exec([]string{"bundle", "config", "--local", "frozen", "true"}, gcp.WithUserAttribution); err != nil { return err } if _, err := ctx.Exec([]string{"bundle", "config", "--local", "without", "development test"}, gcp.WithUserAttribution); err != nil { return err } if _, err := ctx.Exec([]string{"bundle", "config", "--local", "path", localGemsDir}, gcp.WithUserAttribution); err != nil { return err } if _, err := ctx.Exec([]string{"bundle", "install"}, gcp.WithEnv("NOKOGIRI_USE_SYSTEM_LIBRARIES=1", "MALLOC_ARENA_MAX=2", "LANG=C.utf8"), gcp.WithUserAttribution); err != nil { return err } // Find any gem-installed binary directory and symlink as a static path foundBinDirs, err := ctx.Glob(".bundle/gems/ruby/*/bin") if err != nil { return fmt.Errorf("finding bin dirs: %w", err) } if len(foundBinDirs) > 1 { return fmt.Errorf("unexpected multiple gem bin dirs: %v", foundBinDirs) } else if len(foundBinDirs) == 1 { if err := ctx.Symlink(filepath.Join(ctx.ApplicationRoot(), foundBinDirs[0]), localBinDir); err != nil { return err } } // Move the built .bundle directory into the layer if err := ctx.RemoveAll(bundleOutput); err != nil { return err } if _, err := ctx.Exec([]string{"mv", ".bundle", bundleOutput}, gcp.WithUserTimingAttribution); err != nil { return err } } // Always link local .bundle directory to the actual installation stored in the layer. if err := ctx.Symlink(bundleOutput, ".bundle"); err != nil { return err } return nil } // checkCache checks whether cached dependencies exist and match. func checkCache(ctx *gcp.Context, l *libcnb.Layer, opts ...cache.Option) (bool, error) { result, err := ctx.Exec([]string{"ruby", "-v"}) if err != nil { return false, err } currentRubyVersion := result.Stdout opts = append(opts, cache.WithStrings(currentRubyVersion)) hash, cached, err := cache.HashAndCheck(ctx, l, dependencyHashKey, opts...) if err != nil { return false, err } if cached { return true, nil } ctx.Logf("Installing application dependencies.") cache.Add(ctx, l, dependencyHashKey, hash) // Update the layer metadata. ctx.SetMetadata(l, rubyVersionKey, currentRubyVersion) return false, nil }