We tend to have both .ruby-version file and a ruby directive in your Gemfile, and when you might choose one over the other, or even use both. This gets to the heart of how Ruby version management works in a Rails project, and it involves understanding the roles of different tools.
tl;dr: Between Ruby on Rails 5.2 (2017) and 7.1 (2024), we had both .ruby-version and ruby directive in Gemfile, but since 7.2 (2024) we should no more have ruby directive in Gemfile. We should manage the Ruby version only in .ruby-version for your Rails applications even if you want to add contstraints.
1. The Role of .ruby-version (and version managers)
-
Interpreter Selection: The primary purpose of
.ruby-versionis to tell Ruby version managers (likerbenv,chruby,asdf) which Ruby installation to use when you're in a project directory. It's the mechanism that activates a specific Ruby.- When you
cdinto a directory containing a.ruby-versionfile, and you're using a version manager, the version manager reads the file. - It then modifies your shell's
PATHenvironment variable (and potentially other environment variables) to point to the binaries (likeruby,gem,bundle,rails) of the specified Ruby version. - This ensures that when you run
ruby -v, you see the version specified in.ruby-version, and that any commands you execute (likebundle install) will use that specific Ruby installation.
- When you
System-Wide vs. Project-Specific: Version managers are designed to allow you to have multiple Ruby versions installed on your system simultaneously.
.ruby-versionprovides a project-specific override. You might have a system-wide default Ruby, but each project can specify its own requirement.Outside of Bundler's Scope: Crucially,
.ruby-versionis primarily handled by external tools (the version managers). Bundler doesn't directly interpret or enforce.ruby-versionin the same way it enforces therubydirective in theGemfile. Bundler sees the result (the active Ruby version), but it doesn't set it.
2. The Role of the ruby Directive in Gemfile
-
Dependency Constraint and Compatibility: The
rubydirective inGemfileserves a different purpose. It's a constraint that Bundler uses during dependency resolution. It tells Bundler:- "This project requires at least this version of Ruby."
- "When resolving gem dependencies, consider only gem versions that are compatible with this specified Ruby version."
-
Bundler's Responsibility: Bundler enforces this constraint. If you try to
bundle installwith an incompatible Ruby version (one that doesn't meet the requirement in theGemfile), Bundler will raise an error and refuse to proceed.
# Gemfile ruby "3.4.2"If you were to run
bundle installwith Ruby 3.4.1 active, Bundler would error out. -
Platform-Specific Rubies (Optional): The
rubydirective can also be used to specify platform-specific Ruby implementations:
# Gemfile ruby "3.1.6", engine: "jruby", engine_version: "9.4.12.0"This tells Bundler that the project requires JRuby 9.4.12.0, which is compatible with Ruby 3.1.6 (actually Ruby 3.1).
Doesn't Activate a Ruby: The
rubydirective inGemfiledoesn't change your active Ruby version. It's a declaration of a requirement, not a command to switch interpreters. This is the key difference from.ruby-version.
3. Why Both Might Be Used (and When)
Here's a breakdown of common scenarios:
-
Scenario 1:
.ruby-versionOnly (Common in older projects or simpler setups)- How it works: You rely solely on the version manager and
.ruby-versionto set the correct Ruby. You don't explicitly state the Ruby version in theGemfile. - Pros: Simpler setup, especially if you consistently use a version manager.
- Cons: Less explicit. Someone working on the project without a version manager might accidentally use the wrong Ruby and not realize it until runtime errors occur. Bundler won't warn them. Less portable to environments without the same version manager setup.
- How it works: You rely solely on the version manager and
-
Scenario 2:
rubyinGemfileOnly (Less Common)- How it works: You rely on the
rubydirective inGemfileto enforce the Ruby version. You don't use a.ruby-versionfile. You might manually switch Ruby versions (perhaps with your OS's package manager) or rely on a system-wide default Ruby that happens to match theGemfilerequirement. - Pros: Bundler will always enforce the Ruby version.
- Cons: Very inconvenient. You have to manually ensure the correct Ruby is active before running any commands. Prone to errors if you forget. Not suitable for projects requiring different Ruby versions.
- How it works: You rely on the
-
Scenario 3: Both
.ruby-versionandrubyinGemfile(Best Practice, and now the Rails default)- How it works: This was the recommended approach, and it's how Rails 5.2 through 7.1 generates new projects. You used to use both files.
- Pros:
- Best of both worlds:
.ruby-versionhandles the automatic switching of Ruby versions, making development convenient. Therubydirective inGemfileacts as a safety net and ensures Bundler only resolves compatible dependencies. - Explicit and Enforced: The required Ruby version is clearly stated in two places, reducing ambiguity.
- Portable: The
Gemfileconstraint works regardless of whether a version manager is used. - Consistency: This combination helps ensure consistency between development, testing, and production environments.
- Best of both worlds:
- Cons: Slightly more complex setup (but the complexity is justified by the benefits). You need to keep the versions in both files in sync. (Rails 7.2+'s
--skip-ruby-versionoption forrails newcan help in specific cases like using Devcontainers, where.ruby-versionmight be redundant).
-
Scenario 4: .ruby-version file is present, and the ruby directive within the Gemfile is not used. However, a .gemspec file exists, specifying the required_ruby_version.
-
How it Works:
-
.ruby-version: Manages the active Ruby version as described before. -
.gemspec: If your project is also intended to be packaged as a gem (even if it's primarily an application), the.gemspecfile can include arequired_ruby_versionsetting. This is similar to therubydirective in theGemfile, but it applies when your project is installed as a gem into another project. - Bundler respects
required_ruby_versionfrom a.gemspec: When you runbundle install, Bundler will check therequired_ruby_versionin your.gemspec(if it exists) and ensure the currently active Ruby version satisfies that requirement. It acts like arubydirective in theGemfile.
-
- Pros:
- Good practice if your project could potentially be used as a gem by others. Ensures compatibility when installed as a dependency.
- Avoids redundancy: If the version is already accurately specified in the
.gemspec, there's less need to duplicate it in theGemfile.
- Cons:
- Slightly less explicit within the Gemfile itself. A developer looking only at the
Gemfilemight not immediately see the Ruby version constraint. -
.gemspecis primarily for gem packaging; if your project is only an application and never intended to be a gem, it might feel a bit out of place to use.gemspecfor this. - This scenario is acceptable, though generally scenario 3 is preferred.
- Slightly less explicit within the Gemfile itself. A developer looking only at the
-
How it Works:
Set Ruby version in Gemfile and .ruby-version by default rails/rails#30016
rails/rails#30016 introduced ruby "3.4.2" in Gemfile file in 2017 targeting for Ruby on Rails 5.2.0. This is applied only when new Rails applications are created through rails new command.
https://github.com/rails/rails/pull/30016
Permanently remove ruby from Gemfile rails/rails#50914
7 years after its inception in 2017, rails/rails#50914 permanently removed the ruby "3.4.2" directive from Gemfile file in 2024 targeting for Ruby on Rails 7.2.0. This change looks like a so minor problem
The original commit message was misleading but @rafaelfranca explained correctly as follows:
https://github.com/rails/rails/pull/50914#issuecomment-2529739641
It is not temporary anymore. We decided to not generate ruby version in the gemfile
Personally with this change in Feb 2024 I am so glad to get rid of ruby DSL from Gemfile, which bothers me and our developers. 🎉🎉🎉
In summary, we had to use both .ruby-version and the ruby directive in Gemfile for the best combination of convenience, explicitness, and safety according to rails new by default. This had ensured that your project uses the correct Ruby interpreter and that Bundler enforces compatibility during dependency resolution. If you are using a devcontainer, you should manage the Ruby version in .ruby-version and .devcontainer/Dockerfile files by your own self, by once removing ruby directive in Gemfile and the Dockerfile to manage the Ruby version. Since then, you are free from these constraints with the balanced configurations. Thanks Rails team for these long-term improvements ❤️❤️❤️
Top comments (0)