Skip to content

Commit 2893e03

Browse files
committed
Configure gem install location via GEM_*
The previous logic used the `BUNDLE_PATH` environment variable to direct bundler where to install gems. This had the side effect of adding additional paths to the directory structure, so the files wouldn't be in `<layer-path>` they would be in a `<layer-path>/ruby/<major>.<minor>.0/` directory such as `<layer-path>/ruby/3.4.0`. The classic buildpack handles this by shelling out to Ruby https://github.com/heroku/heroku-buildpack-ruby/blob/b3ccc41885135ae495c604a512b523c81241914d/lib/language_pack/ruby.rb#L157. This change diverges and takes an alternative approach, using the `GEM_HOME` and `GEM_PATH` environment variables to configure installation location rather than configuring bundler directly. When `bundle install` is run, it will install into the first `GEM_PATH` (there can be multiple). This install will be direct (without any additional directories under it). This is what we want. This also allows us to remove `BUNDLE_BIN` which is no longer needed (bundler will install binaries into `GEM_PATH/bin`). - `BUNDLE_DEPLOYMENT` does more than forces the `Gemfile.lock` to be frozen, it also installs gems into `vendor/bundle`, which we don't want. This has been changed to `BUNDLE_FROZEN=1`. Unfortunately, removing `BUNDLE_PATH` causes the automatic clean after install logic (`BUNDLE_CLEAN=1`) to error with a warning saying that it is unsafe because there's no explicit path. To work around this, I added a manual call to `bundle clean --force` after the `bundle install`. This requires I also remove the `BUNDLE_CLEAN=1` environment variable. Because this changes the structure of the underlying gems on disk, I need to clear the cache manually by updating the key to `v2`.
1 parent b5fd48a commit 2893e03

File tree

4 files changed

+41
-47
lines changed

4 files changed

+41
-47
lines changed

buildpacks/ruby/CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Gem install behavior and configuration (https://github.com/heroku/buildpacks-ruby/pull/402)
13+
- Gem install path is now configured with `GEM_HOME` and `GEM_PATH` instead of `BUNDLE_PATH`.
14+
- Cleaning gems is now accomplished via running `bundle clean --force`. Previously it was accomplished by setting `BUNDLE_CLEAN=1`.
15+
- The `BUNDLE_DEPLOYMENT=1` environment variable is changed to `BUNDLE_FROZEN=1`.
16+
- The `BUNDLE_BIN` environment variable is no longer set.
17+
1018
## [5.1.0] - 2025-02-28
1119

1220
### Changed

buildpacks/ruby/src/layers/bundle_install_layer.rs

+16-28
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const SKIP_DIGEST_ENV_KEY: &str = "HEROKU_SKIP_BUNDLE_DIGEST";
4242
/// A failsafe, if a programmer made a mistake in the caching logic, rev-ing this
4343
/// key will force a re-run of `bundle install` to ensure the cache is correct
4444
/// on the next build.
45-
pub(crate) const FORCE_BUNDLE_INSTALL_CACHE_KEY: &str = "v1";
45+
pub(crate) const FORCE_BUNDLE_INSTALL_CACHE_KEY: &str = "v2";
4646

4747
pub(crate) fn handle<W>(
4848
context: &libcnb::build::BuildContext<RubyBuildpack>,
@@ -99,6 +99,15 @@ where
9999
fun_run::map_which_problem(error, cmd.mut_cmd(), env.get("PATH").cloned())
100100
})
101101
.map_err(RubyBuildpackError::BundleInstallCommandError)?;
102+
103+
bullet
104+
.time_cmd(
105+
Command::new("bundle")
106+
.args(["clean", "--force"])
107+
.env_clear()
108+
.envs(&env),
109+
)
110+
.map_err(RubyBuildpackError::BundleInstallCommandError)?;
102111
}
103112
InstallState::Skip(checked) => {
104113
let bundle_install = style::value("bundle install");
@@ -252,20 +261,14 @@ fn layer_env(layer_path: &Path, app_dir: &Path, without_default: &BundleWithout)
252261
.chainable_insert(
253262
Scope::All,
254263
ModificationBehavior::Override,
255-
"BUNDLE_PATH", // Directs bundler to install gems to this path.
264+
"GEM_HOME", // Tells bundler where to install gems, along with GEM_PATH
256265
layer_path,
257266
)
258-
.chainable_insert(
259-
Scope::All,
260-
ModificationBehavior::Override,
261-
"BUNDLE_BIN", // Install executables for all gems into specified path.
262-
layer_path.join("bin"),
263-
)
264267
.chainable_insert(Scope::All, ModificationBehavior::Delimiter, "GEM_PATH", ":")
265268
.chainable_insert(
266269
Scope::All,
267270
ModificationBehavior::Prepend,
268-
"GEM_PATH", // Tells Ruby where gems are located. Should match `BUNDLE_PATH`.
271+
"GEM_PATH", // Tells Ruby where gems are located.
269272
layer_path,
270273
)
271274
.chainable_insert(
@@ -283,13 +286,7 @@ fn layer_env(layer_path: &Path, app_dir: &Path, without_default: &BundleWithout)
283286
.chainable_insert(
284287
Scope::All,
285288
ModificationBehavior::Override,
286-
"BUNDLE_CLEAN", // After successful `bundle install` bundler will automatically run `bundle clean`
287-
"1",
288-
)
289-
.chainable_insert(
290-
Scope::All,
291-
ModificationBehavior::Override,
292-
"BUNDLE_DEPLOYMENT", // Requires the `Gemfile.lock` to be in sync with the current `Gemfile`.
289+
"BUNDLE_FROZEN", // Requires the `Gemfile.lock` to be in sync with the current `Gemfile`.
293290
"1",
294291
);
295292
// CAREFUL: Changes to these ^^^^^^^ environment variables
@@ -306,14 +303,7 @@ fn display_name(cmd: &mut Command, env: &Env) -> String {
306303
fun_run::display_with_env_keys(
307304
cmd,
308305
env,
309-
[
310-
"BUNDLE_BIN",
311-
"BUNDLE_CLEAN",
312-
"BUNDLE_DEPLOYMENT",
313-
"BUNDLE_GEMFILE",
314-
"BUNDLE_PATH",
315-
"BUNDLE_WITHOUT",
316-
],
306+
["BUNDLE_FROZEN", "BUNDLE_GEMFILE", "BUNDLE_WITHOUT"],
317307
)
318308
}
319309

@@ -439,12 +429,10 @@ mod test {
439429

440430
let actual = commons::display::env_to_sorted_string(&env);
441431
let expected = r"
442-
BUNDLE_BIN=layer_path/bin
443-
BUNDLE_CLEAN=1
444-
BUNDLE_DEPLOYMENT=1
432+
BUNDLE_FROZEN=1
445433
BUNDLE_GEMFILE=app_path/Gemfile
446-
BUNDLE_PATH=layer_path
447434
BUNDLE_WITHOUT=development:test
435+
GEM_HOME=layer_path
448436
GEM_PATH=layer_path
449437
";
450438
assert_eq!(expected.trim(), actual.trim());

buildpacks/ruby/tests/integration_test.rs

+15-14
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ fn test_default_app_ubuntu20() {
8181
assert_contains!(context.pack_stderr, "# Heroku Ruby Buildpack");
8282
assert_contains!(
8383
context.pack_stderr,
84-
r#"`BUNDLE_BIN="/layers/heroku_ruby/gems/bin" BUNDLE_CLEAN="1" BUNDLE_DEPLOYMENT="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_PATH="/layers/heroku_ruby/gems" BUNDLE_WITHOUT="development:test" bundle install`"#);
84+
r#"`BUNDLE_FROZEN="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_WITHOUT="development:test" bundle install`"#
85+
);
8586

8687
assert_contains!(context.pack_stderr, "Installing puma");
8788

@@ -90,7 +91,7 @@ fn test_default_app_ubuntu20() {
9091
let command_output = context.run_shell_command(
9192
indoc! {"
9293
set -euo pipefail
93-
printenv | sort | grep -vE '(_|HOME|HOSTNAME|OLDPWD|PWD|SHLVL|SECRET_KEY_BASE)='
94+
printenv | sort | grep -vE '(_|^HOME|HOSTNAME|OLDPWD|PWD|SHLVL|SECRET_KEY_BASE)='
9495
9596
# Output command + output to stdout
9697
export BASH_XTRACEFD=1; set -o xtrace
@@ -101,13 +102,11 @@ fn test_default_app_ubuntu20() {
101102
assert_empty!(command_output.stderr);
102103
assert_eq!(
103104
formatdoc! {"
104-
BUNDLE_BIN=/layers/heroku_ruby/gems/bin
105-
BUNDLE_CLEAN=1
106-
BUNDLE_DEPLOYMENT=1
105+
BUNDLE_FROZEN=1
107106
BUNDLE_GEMFILE=/workspace/Gemfile
108-
BUNDLE_PATH=/layers/heroku_ruby/gems
109107
BUNDLE_WITHOUT=development:test
110108
DISABLE_SPRING=1
109+
GEM_HOME=/layers/heroku_ruby/gems
111110
GEM_PATH=/layers/heroku_ruby/gems:/layers/heroku_ruby/bundler
112111
JRUBY_OPTS=-Xcompile.invokedynamic=false
113112
LD_LIBRARY_PATH=/layers/heroku_ruby/binruby/lib
@@ -190,11 +189,10 @@ fn test_default_app_ubuntu20() {
190189
assert_eq!(
191190
r"
192191
$ echo $PATH
193-
/layers/heroku_ruby/gems/ruby/<x.y.z>/bin:/workspace/bin:/layers/heroku_ruby/gems/bin:/layers/heroku_ruby/bundler/bin:/layers/heroku_ruby/binruby/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
192+
/layers/heroku_ruby/gems/bin:/workspace/bin:/layers/heroku_ruby/bundler/bin:/layers/heroku_ruby/binruby/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
194193
$ which -a rake
195-
/layers/heroku_ruby/gems/ruby/<x.y.z>/bin/rake
196-
/workspace/bin/rake
197194
/layers/heroku_ruby/gems/bin/rake
195+
/workspace/bin/rake
198196
/layers/heroku_ruby/binruby/bin/rake
199197
/usr/bin/rake
200198
/bin/rake
@@ -203,7 +201,7 @@ fn test_default_app_ubuntu20() {
203201
/usr/bin/ruby
204202
/bin/ruby
205203
".trim(),
206-
Regex::new(r"/layers/heroku_ruby/gems/ruby/\d+\.\d+\.\d+/bin").unwrap().replace_all(&rake_output, "/layers/heroku_ruby/gems/ruby/<x.y.z>/bin").trim()
204+
rake_output.trim()
207205
);
208206

209207
let command_output = rebuild_context.run_shell_command(
@@ -240,7 +238,8 @@ fn test_default_app_ubuntu22() {
240238
assert_contains!(context.pack_stderr, "# Heroku Ruby Buildpack");
241239
assert_contains!(
242240
context.pack_stderr,
243-
r#"`BUNDLE_BIN="/layers/heroku_ruby/gems/bin" BUNDLE_CLEAN="1" BUNDLE_DEPLOYMENT="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_PATH="/layers/heroku_ruby/gems" BUNDLE_WITHOUT="development:test" bundle install`"#);
241+
r#"`BUNDLE_FROZEN="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_WITHOUT="development:test" bundle install`"#
242+
);
244243

245244
assert_contains!(context.pack_stderr, "Installing puma");
246245
},
@@ -259,7 +258,8 @@ fn test_default_app_latest_distro() {
259258
assert_contains!(context.pack_stderr, "# Heroku Ruby Buildpack");
260259
assert_contains!(
261260
context.pack_stderr,
262-
r#"`BUNDLE_BIN="/layers/heroku_ruby/gems/bin" BUNDLE_CLEAN="1" BUNDLE_DEPLOYMENT="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_PATH="/layers/heroku_ruby/gems" BUNDLE_WITHOUT="development:test" bundle install`"#);
261+
r#"`BUNDLE_FROZEN="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_WITHOUT="development:test" bundle install`"#
262+
);
263263

264264
assert_contains!(context.pack_stderr, "Installing puma");
265265

@@ -340,7 +340,7 @@ DEPENDENCIES
340340
assert_contains!(context.pack_stderr, "# Heroku Ruby Buildpack");
341341
assert_contains!(
342342
context.pack_stderr,
343-
r#"`BUNDLE_BIN="/layers/heroku_ruby/gems/bin" BUNDLE_CLEAN="1" BUNDLE_DEPLOYMENT="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_PATH="/layers/heroku_ruby/gems" BUNDLE_WITHOUT="development:test" bundle install`"#
343+
r#"`BUNDLE_FROZEN="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_WITHOUT="development:test" bundle install`"#
344344
);
345345
assert_contains!(context.pack_stderr, "Ruby version `3.1.4-jruby-9.4.8.0` from `Gemfile.lock`");
346346
});
@@ -361,7 +361,8 @@ fn test_ruby_app_with_yarn_app() {
361361
assert_contains!(context.pack_stderr, "# Heroku Ruby Buildpack");
362362
assert_contains!(
363363
context.pack_stderr,
364-
r#"`BUNDLE_BIN="/layers/heroku_ruby/gems/bin" BUNDLE_CLEAN="1" BUNDLE_DEPLOYMENT="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_PATH="/layers/heroku_ruby/gems" BUNDLE_WITHOUT="development:test" bundle install`"#);
364+
r#"`BUNDLE_FROZEN="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_WITHOUT="development:test" bundle install`"#
365+
);
365366
}
366367
);
367368
}

docs/application_contract.md

+2-5
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Once an application has passed the detect phase, the build phase will execute to
3838
- `Gemfile.lock`
3939
- User configurable environment variables.
4040
-To always run `bundle install` even if there are changes if the environment variable `HEROKU_SKIP_BUNDLE_DIGEST=1` is found.
41-
- We will always run `bundle clean` after a successful `bundle install` via setting `BUNDLE_CLEAN=1` environment variable.
41+
- We will always run `bundle clean` after a successful `bundle install`.
4242
- We will always cache the contents of your gem dependencies.
4343
- We will always invalidate the dependency cache if your distribution name or version (operating system) changes.
4444
- We will always invalidate the dependency cache if your CPU architecture (i.e. amd64) changes.
@@ -71,11 +71,8 @@ Once an application has passed the detect phase, the build phase will execute to
7171
- `SECRET_KEY_BASE=${SECRET_KEY_BASE:-<generate a secret key>}` - In Rails 4.1+ apps a value is needed to generate cryptographic tokens used for a variety of things. Notably this value is used in generating user sessions so modifying it between builds will have the effect of logging out all users. This buildpack provides a default generated value. You can override this value.
7272
- `BUNDLE_WITHOUT=development:test` - Tells bundler to not install `development` or `test` groups during `bundle install`. You can override this value.
7373
- Environment variables modified - In addition to the default list this is a list of environment variables that the buildpack modifies:
74-
- `BUNDLE_BIN=<bundle-path-dir>/bin` - Install executables for all gems into specified path.
75-
- `BUNDLE_CLEAN=1` - After successful `bundle install` bundler will automatically run `bundle clean` to remove all stale gems from previous builds that are no longer specified in the `Gemfile.lock`.
76-
- `BUNDLE_DEPLOYMENT=1` - Requires `Gemfile.lock` to be in sync with the current `Gemfile`.
74+
- `BUNDLE_FROZEN=1` - Requires `Gemfile.lock` to be in sync with the current `Gemfile`.
7775
- `BUNDLE_GEMFILE=<app-dir>/Gemfile` - Tells bundler where to find the `Gemfile`.
78-
- `BUNDLE_PATH=<bundle-path-dir>` - Directs bundler to install gems to this path
7976
- `DISABLE_SPRING="1"` - Spring is a library that attempts to cache application state by forking and manipulating processes with the goal of decreasing development boot time. Disabling it in production removes significant problems [details](https://devcenter.heroku.com/changelog-items/1826).
8077
- `GEM_PATH=<bundle-path-dir>` - Tells Ruby where gems are located.
8178
- `MALLOC_ARENA_MAX=2` - Controls glibc memory allocation behavior with the goal of decreasing overall memory allocated by Ruby [details](https://devcenter.heroku.com/changelog-items/1683).

0 commit comments

Comments
 (0)