As an iOS engineer, CocoaPods is something we’re all familiar with. In our daily development, however, we probably don’t know much about the Ruby language in which CocoaPods is written, let alone Bundler or RVM. Therefore, when we encounter some Ruby environment-related problems, we may have no idea what is going on. If you’re curious about what these two tools do, in this article I’ll try to explain RVM/Bundler from a very simple point of view to give you a deeper understanding of Ruby environment management.

TLDR

  • Use RVM to install Ruby
  • gem install rubygems-bundler && gem regenerate_binstubsIt saves you from having to be there every timepod installBefore you addbundle execThe pain of

Where did the Ruby we used come from?

As we all know, macOS comes with Ruby. That is, when we get a new MacBook Pro, go to whereis Ruby and open a terminal to execute whereis Ruby, we get something like /usr/bin/ruby.

In the current Version of macOS 10.14, the system comes with Ruby version 2.3.7.

Why use RVM?

Gem install Cococapods gem install Cococapods gem install Cococapods

You don't have write permissions for the /Library/Ruby/Gems/2.3.0 directory
Copy the code

Why did this happen? Because gem is the default package manager for Ruby, it installs all downloaded Gems in a directory called gem Path. For Ruby, this directory is /Library/Ruby/Gems/2.3.0. This is a directory that requires sudo enabled to write to. This causes sudo to be added before the command every time gem install is executed.

To solve this problem, we need to have the Gem Path point to a directory where we have write permission. The easy and straightforward way is to install a new Ruby using Homebrew.

It seems perfect, but there’s a problem: how do we constrain everyone to use the same version of Ruby?

The answer is to use Ruby’s version management tools. In the case of RVM, when you install RVM, every CD command you execute on the command line is actually replaced by RVM. RVM checks each directory switch to see if there is a.ruby-version file in the current directory, and if so, to see if the ruby in use is the version specified in the file. If not, he will give a warning like Required ruby-x.x.x is not installed.

In the early stages of our project, in addition to using Cocoapods, we also needed to use Ruby to write some packaging and publishing scripts. At that time, the Ruby version provided by the system was relatively low (2.0.0), which was not easy to develop. Not only can we easily install a new version of Ruby, we can also use.ruby-version to ensure that everyone can use the same version of Ruby (albeit with a weak constraint).

At this point, I’m sure you understand the need to use RVM in our project. Which brings us to our second question: Why use a Bundler?

Why use Bundler?

To answer this question, we need to turn our attention to gems and recall the problems they were created to solve.

Problems to be solved by GEM

In Ruby, if you want to use content from another Ruby file, you need to use the require keyword to load content from another Ruby file. Require looks for the corresponding file in Ruby’s default $LOAD_PATH. You can see what $LOAD_PATH has in current Ruby by executing ruby-e ‘puts $LOAD_PATH’.

For example, if you write a simple Ruby script:

require 'foo'
Copy the code

When the require ‘foo’ line is executed, Ruby looks for a file called foo.rb in all directories that appear in $LOAD_PATH. If so, load the contents of the file. If no such file is found in all $LOAD_PATH, the Ruby interpreter throws an exception. Exceptions usually look like this:

LoadError - cannot load such file -- foo
Copy the code

Before gems, if you wanted to use Ruby scripts written by someone else, you had to manually download those scripts and put them in a directory in $LOAD_PATH so that you could use someone else’s script files correctly in your own scripts. This code distribution process is very primitive and cumbersome.

To solve this problem, gem came out of nowhere with a script distribution solution like this:

  1. Start by using GemSpec to describe the meta information for the script you are about to distribute
  2. Using the commands provided by gem, package the script into a.gem file (which is essentially a POSIX tar archive) and upload it to the server
  3. When someone else wants to use your script, do itgem installCan be

This is easy to understand, so let’s focus on what happens after gem Install is implemented.

When you execute gem install foo, the gem will download foo.gem for you, unzip it, and place it in a directory. This directory is usually a subdirectory of the Gem Path we mentioned earlier, which we will temporarily call Gems Install Path. If foo’s GemSpec declares dependencies on other gems, gem Install Foo will also help you download the gems that Foo depends on.

What Gem Install does is very simple. But gem hasn’t completely solved our problem yet: gems installed by gem Install don’t exist in $LAOD_PATH, and our Ruby script still doesn’t reference them properly.

In order to fix this problem, gem changed the implementation of require in Ruby after it was installed, so that when require executed, in addition to $LOAD_PATH, In Gems, the Install Path find file (you can perform the gem env | grep – A2 ‘gem PATHS’ find your gem installation Path, Gems Install Path in the Gems subdirectory of the directory). When gem finds the file in the GEMS INSTALL PATH, it adds the PATH to $LOAD_PATH and calls Ruby’s original require. Because of the new path in $LOAD_PATH, require will now correctly load into the gem file you installed.

Here we can do a small experiment, find a directory without Gemfile to execute irB, and then type in the following sequence:

old_load_path = $LOAD_PATH.dup
require 'cocoapods'
new_load_path = $LOAD_PATH.dup
# Execute the following code to see how much LOAD_PATH changes
"new: #{new_load_path.count} old: #{old_load_path.count}"
# Execute the following code to see what has changed to LOAD_PATH. You will see cocoapods and the directory where its dependent libraries are located
new_load_path - old_load_path
Copy the code

At this point gem has solved the problem of distributing Ruby scripts perfectly. When you want to use any gem that someone else has already provided, simply type gem Install and your script will happily use that gem.

New problems with gem

So far, so good, but as Ruby has been introduced to large projects, Ruby developers have found a new problem: when your project relies on dozens of gems, it takes dozens of gem installs to get the environment right.

Developers can’t stand this, so they start using various scripts to simplify the process. These scripts might be called setup.sh, and they look something like this:

gem install foo
gem install bar
Copy the code

For the time being we can call a setup.sh file like this a Gem List file because it is a List full of all the gems you need to install 🤓🤓🤓.

When Ruby developers solved the problem of bulk gem installation, they found a new problem: multiple versions of the environment are not isolated.

What do you mean? Let’s take an example to illustrate this problem.

Let’s say you’re A Ruby developer, and you’re maintaining your project A, where you use version 2.0.0 of Foo. After a while, you start maintaining another project, B, which, unfortunately, started with version 3.0.0 of Foo. Here’s the headache: once you’ve configured the environment for project B, you’ll have two versions of Foo’s gem on your machine. At the same time, you will find that your project A will not run because when you run project A, the gem will find the latest version of multiple releases by default, so you use 3.0.0 foo instead of 2.0.0 for project A.

So all sorts of interesting but frustrating things happen: your project might be fine on your local server, but it just won’t work on the server. You check for days and discover that another project on the server has a higher version of gem installed, and the environment on the server cannot run your project at all. You’re miserable, you’re desperate, but there’s nothing you can do 🤬🤬🤬.

Even if you only maintain one project, since the Gem version number is not specified in your Gem List file, it is very likely that the Gem installed from this Gem List file a week ago will be completely different from the Gem installed a week later. It’s a new computer. We want you to spend a week configuring the dependencies for your project, if all goes well.”

Bundler’s solution

To address these gem problems, Bundler comes out of the blue and provides developers with two lifesaving commands:

  • bundle install
  • bundle exec

Bundle Install provides a convenient way to install multiple gems at once. After installing the bundle, Bundler installs all the gems declared in the Gemfile file, the Gem List file, and saves the final version number in gemfile. lock. Ensure that the same version of gem is installed on different machines at different times when bundle Install is executed.

Bundle Exec solves the problem of non-isolation in multi-version environments. When you perform bundle exec, Bundler removes all the irrelevant gem paths from $LOAD_PATH. Lock (if gemfile. lock does not exist, a gemfile. lock will create a gemfile. lock), and ensure that $LOAD_PATH only contains the path to gemfile. lock that has fixed versions. You can execute the following two lines of code to see the difference between $LOAD_PATH:

bundle exec ruby -e 'puts $LOAD_PATH'
ruby -e 'puts $LOAD_PATH'
Copy the code

New problems with Bundler

So far, Bundler has solved the problem of gem installation and isolation, but it also introduces a new problem: every time we execute a Ruby related command, we have to type in bundle exec 🤦🏻♂️🤦🏻♂️🤦🏻♂️.

Fortunately, Ruby developers are lazy and have developed a new gem, RubyGems-Bundler, to solve this problem. Once you install the gem, rubygems-Bundler checks for gemfiles in the current directory and parent directory before executing any gem installation on the command line. If it does, it automatically prefixes your command line with bundle exec. Perfect solution to the problem.

Tip: Rubygems-Bundler is installed by default when installing Ruby on RVM versions 1.11.0 and older. You can check if you have the gem installed by checking gem List rubygems-bundler. If you install Ruby with Homebrew, you won’t get this hidden benefit.

Extended exercise: Let’s take a look at some common errors. Now do you know what happened?

Practice a

LoadError - cannot load such file -- macho
Copy the code

Answer: The macho file is not in $LOAD_PATH, so the Ruby program fails. If there is a Gemfile, you should bundle install first. If not, manually gem Install Macho

Exercise 2

Could not find proper version of Cocoapodsin any of the sources
Run `bundle install` to install missing gems.
Copy the code

Gemfile.lock specifies cocoapods as 1.1.1, but Paths does not install Cocoapods 1.1.1

Practice three

Required ruby-2.3.7 is not installed.
To install do: 'the RVM install "ruby - 2.3.7"'
Copy the code

Answer: The.ruby-version in the current directory specifies that the Ruby version should be 2.3.7, but 2.3.7 is not installed on the current machine. To avoid future problems, run RVM install 2.3.7

reference

  • Rbenv — How it Works
  • rbenv vs rvm
  • A Ruby workflow with RVM and Bundler
  • Bundler: The best way to manage a Ruby application’s gems
  • How Bundler Works: A History of Ruby Dependency Management
  • A History of Bundles: 2010 to 2017
  • Understanding ruby load, require, gems, Bundler and rails autoloading from the bottom up
  • How does Bundler work, anyway?