cmd/ruby/rubygems/main.go (147 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 (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/GoogleCloudPlatform/buildpacks/pkg/fetch"
"github.com/GoogleCloudPlatform/buildpacks/pkg/fileutil"
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
"github.com/GoogleCloudPlatform/buildpacks/pkg/ruby"
"github.com/buildpacks/libcnb/v2"
)
// source: https://rubygems.org/pages/download
var (
rubygemsURL = "https://rubygems.org/rubygems/rubygems-3.3.15.tgz"
bundler1Version = "1.17.3"
bundler2Version = "2.3.15"
)
const (
layerName = "rubygems"
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 {
layer, err := ctx.Layer(layerName, gcp.BuildLayer, gcp.CacheLayer, gcp.LaunchLayerUnlessSkipRuntimeLaunch)
if err != nil {
return fmt.Errorf("creating layer: %w", err)
}
if err = installRubygems(ctx, layer); err != nil {
return err
}
// Install bundler1 for older Ruby runtimes if required. Ruby 3.2+ does not support it.
supportsBundler1, err := ruby.SupportsBundler1(ctx)
if err != nil {
return err
}
if supportsBundler1 {
usingBundler1, err := isUsingBundler1(ctx)
if err != nil {
return err
}
if usingBundler1 {
if err = installBundler1(ctx, layer); err != nil {
return err
}
}
}
// this makes ruby use the gem and bundler from the layer, instead of the default location
layer.SharedEnvironment.Default("RUBYLIB", filepath.Join(layer.Path, "lib"))
// this makes gem aware of bundler in the layer
layer.SharedEnvironment.Default("GEM_PATH", fmt.Sprintf("%s:$GEM_PATH", layer.Path))
// this ensures gem, bundle, and bundler commands are used from the <layer>/bin
layer.SharedEnvironment.Prepend("PATH", string(os.PathListSeparator), filepath.Join(layer.Path, "bin"))
// stop bundler from using load to launch exec. This loads the system installed bundler otherwise
layer.SharedEnvironment.Prepend("BUNDLE_DISABLE_EXEC_LOAD", string(os.PathListSeparator), "1")
return nil
}
func isUsingBundler1(ctx *gcp.Context) (bool, error) {
lockFile := ""
exists, err := ctx.FileExists("Gemfile.lock")
if err != nil {
return false, err
}
if exists {
lockFile = filepath.Join(ctx.ApplicationRoot(), "Gemfile.lock")
} else {
exists, err = ctx.FileExists("gems.locked")
if err != nil {
return false, err
}
if exists {
lockFile = filepath.Join(ctx.ApplicationRoot(), "gems.locked")
} else {
return false, nil
}
}
version, err := ruby.ParseBundlerVersion(lockFile)
if err != nil {
return false, err
}
return strings.HasPrefix(version, "1."), nil
}
// installBundler1 installs bundler {bundler1Version} inside the rubygems layer
func installBundler1(ctx *gcp.Context, layer *libcnb.Layer) error {
ctx.Logf("Installing bundler %s since the Gemfile.lock BUNDLED WITH uses 1.x.x", bundler1Version)
_, err := ctx.Exec([]string{"gem", "install", fmt.Sprintf("bundler:%s", bundler1Version), "--no-document"},
gcp.WithEnv(fmt.Sprintf("GEM_PATH=%s", layer.Path),
fmt.Sprintf("GEM_HOME=%s", layer.Path)),
gcp.WithUserAttribution,
)
if err != nil {
return fmt.Errorf("installing bundler %s, err: %v", bundler1Version, err)
}
// bundler 1.17.3 won't work if we don't remove the newer bundler that comes with rubygems
if err := os.RemoveAll(filepath.Join(layer.Path, "lib", "bundler")); err != nil &&
!errors.Is(err, os.ErrNotExist) {
return err
}
if err := os.Remove(filepath.Join(layer.Path, "lib", "bundler.rb")); err != nil &&
!errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
// installRubygems installs a newer version of rubygems and bundler
func installRubygems(ctx *gcp.Context, layer *libcnb.Layer) error {
tempDir, err := os.MkdirTemp(layer.Path, "rubygems")
if err != nil {
return fmt.Errorf("creating a temp directory, err: %q", err)
}
defer os.RemoveAll(tempDir)
// Since Ruby 2.5.x has issues with the default RubyGems (3.3.15) and Bunder 2 versions,
// use an older version to maintain functionality.
if ruby.IsRuby25(ctx) {
rubygemsURL = "https://rubygems.org/rubygems/rubygems-3.2.26.tgz"
bundler2Version = "2.2.26"
}
if err = fetch.Tarball(rubygemsURL, tempDir, 1); err != nil {
return fmt.Errorf("fetching rubygems tarball from %s, err: %q", rubygemsURL, err)
}
// this allows us to ship rubygems and bundler separately from the ruby runtime
if _, err = ctx.Exec([]string{"ruby", "setup.rb", "-E", "--no-document", "--destdir", layer.Path, "--prefix", "/"},
gcp.WithWorkDir(tempDir),
gcp.WithUserAttribution,
); err != nil {
return err
}
// this is used to run bundler/setup
// https://github.com/rubygems/rubygems/blob/v3.3.15/bundler/lib/bundler/shared_helpers.rb#L277
destExe := filepath.Join(layer.Path, "exe")
os.MkdirAll(destExe, 0755)
if err = fileutil.MaybeCopyPathContents(
destExe,
filepath.Join(layer.Path, "gems", fmt.Sprintf("bundler-%s", bundler2Version), "exe"),
fileutil.AllPaths,
); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}