Consistent Ruby environment for iOS development

As is the case with most iOS developers, we rely on certain third-party libraries that help us do better stuff with iOS development, deployment to the App Store, and running automated tests via a continuous integration server.

When working in larger development teams - especially as time passes - you tend to encounter problems that come from working with different versions of tools. Sometimes, different tool versions cannot be captured in Podfile because they’re installed via Ruby gems. It’s quite frustrating to find, debug, and fix those things - especially when you’d much rather focus on developing your awesome app.

The Problem

Why not sudo?

Most tutorials use sudo when showing how to use tools like cocoapods or fastlane, mainly because it makes it easier to explain the main topic. But in real, hands-on work, be careful, and try to avoid sudo completely if at all possible.

Why? Because you cannot trust third-party libraries. Imagine one of your dependencies accidentally having code that deletes your entire root directory, for example. If you’re feeling brave, try installing killergem.

And also read an excellent Calabash’s Never install gems with sudo article.

Imagine coming back to the office after vacation. You fire up your machine and figure out that Fastlane has been upgraded to a newer version because Apple has changed the AppStoreConnect API. Now, older fastlane versions expect other API calls to happen and you’re calling to a non-existent endpoint.

Most of the time, the tools’ official documentation will give you a straight answer on how to install them the fastest way possible - typically something like sudo gem install fastlane. But what about your CI machine? If you don’t have an escape plan, your builds will likely fail - and that’s no good. Imagine this happening for cocoaPods or any other gem on which you depend.

Using sudo with gems really should not be considered best practice, and how to deal with this situation the right way.

There are two main reasons why you don’t want to install your gems as root. It is huge security risk (check the end of this article for more info), and they will become part of the global Ruby installation. This can easily lead into issues, as your different projects (you work on more than one thing, right?) can require different versions of gems.

You will have the same issue with other system-wide package managers, such as homebrew, which works great if you want to install one version globally (and don’t need project-specific versions).

The Solution

Fortunately, developers from the world of Ruby have created several useful instruments - many of which are both helpful and unfamiliar or unknown to iOS developers.

The key is to keep all gems required for your project specified somewhere. In fact, you don’t need Homebrew, or anything else at all. You just need ruby gems and a tool called Bundler. It uses a so-called Gemfile, which works similarly to the Podfile that most iOS developers will be familiar with.

For example, if we rely on Cocoapods, XCPretty, and Fastlane, then Gemfile would look like this:

source 'https://Rubygems.org'
gem 'cocoapods'
gem ’xcpretty'
gem 'fastlane'

From now on, if somebody updates any of the dependencies, others get notified by build error and need to run the following command to get the project/pipeline up and working again:

bundle install

🎉 Now all dependencies will be in sync, and you can get back to work on releasing your iOS app. 🎉

How to make it work

The dependency chain for this is pretty straightforward as well. You need 5 things:

  1. RVM or rbenv
  2. A nice version of Ruby (2.6.0, perhaps)
  3. Bundler
  4. Gemfile & Gemfile.lock
  5. Confirmation that you’re using Bundler instead of running “global” gems

RVM & rbenv

RVM and rbenv are tools for managing Ruby versions on your computer that make your Ruby experience much smoother than the system Ruby, which is stuck on 2.3.3 and cannot be upgraded to a higher version without doing some linking and hacking.

As I wrote earlier, the default version of Ruby for macOS is really old and you need to install something newer, future-proof, and more functional than the default macOS Ruby - which sometimes requires sudo to execute commands that can possibly harm your computer. Think of Ruby version managers as iOS Platform, and Ruby implementations as the sandboxed Apps on iOS, and it should make more sense.

Here are some tips and hints to get you set up:

  • Clean up home and system-wide gems (to prevent conflicts in gems)
  • Define .ruby-version in the project folder so RVM/rbenv can use a specific Ruby version for your Ruby scripts
  • Make sure that you are installing RVM/rbenv from trusted source. Check RVM security for example.
  • Initialize RVM/rbenv. Either by restarting terminal or source ~/.rvm/scripts/rvm in case of RVM.
  • When using RVM, we suggest enabling rvm to be easily executed as a function

Bundler

Bundler is a tool that provides a consistent environment for Ruby (and more) projects by tracking and installing the exact gems and versions that specific project needs. It expects existence of Gemfile in your project root directory that contains all the gems and their versions defined inside.

You should use bundler to execute dependencies defined for your local project in Gemfile. Whenever there is some conflict inside the Gemfile.lock, you will be forced to do a bundle install. In case of an update, you will need to run the bundle update [gem] command.

If you don’t have bundler installed, you can get it with gem install bundler, or (even better) use RVM 1.11.0 or newer to get bundler in.

Gemfile

Think of Gemfile as Podfile for dependencies that are outside the scope of your project. For example, you don’t exactly use CocoaPods from within your Swift code, but it’s still there to help you install those pods.

We need this to define our dependencies inside this file and to generate Gemfile.lock with the bundle install command. I also recommend to add those two files (Gemfile.lock and Gemfile) to your version control, because whenever Gemfile.lock changes you need to somehow notify your colleagues.

From this point forward, you should use bundle exec [gem specified in Gemfile] to ensure you are using the right gem version for the given project in the directory. If you ever used Fastlane, you’ve probably experienced the message about considering using bundle exec [gem specified in Gemfile] before.

To ensure your environment is right, you can put the checks written below inside Podfile and Fastfile to run gems with versions specified in Gemfile.

Note that Bundler installs gems to the same folders that you would usually use with gem install, but the Gemfile fetches the specific versions.

At the start of your Podfile insert this line of code:

using_bundler = defined? Bundler
unless using_bundler
    puts "\nPlease re-run using:".red
    puts "  bundle exec pod install\n\n"

    # This will always invoke wrong Podfile error
    # Even if we do raise/abort because that’s the nature of pods
    exit(1)
end

In Fastlane, just put the code inside before_all lane:

before_all do |lane|
    unless ENV.key?("BUNDLE_BIN_PATH")
        UI.user_error!("Please run using bundle exec fastlane.")
    end
end

Another note: As of Fastlane version 2.112.0, you can omit the unless command and use just the ensure_bundle_exec action.

Benefits

  1. Team members and CI machines will always have the correct dependencies (pods and gems) for the current working state of your code.
  2. This is especially useful when code is quickly evolving and some branches are faster and updated to newest versions, while some (those in code review) still rely on older dependencies.
  3. When building the latest code, the developer gets a warning to install gems if Gemfile was updated. So the developer is always up-to-date with dependencies.
  4. Your CI is flawless. Before each build, you just run bundle install and bundle exec pod install in the current folder and you’re always up-to-date with all gem versions, never have to worry about new Fastlane versions, and it fixes some changes in AppStoreConnect API compatibility.
  5. Now you don’t need to keep your dependencies stored in different package managers (combining Homebrew with gems) which would cause a lot of confusion around your environment.

Conclusion

Now you’ll be able to set up a consistent environment for your project. How do you solve similar problems? Let us know what approach works for you!

Please check the original version of this article at