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.
Most tutorials use sudo when showing how to use tools like
fastlane, mainly because it makes it easier to explain the main topic. But in real, hands-on work, be careful, and try to avoid
sudocompletely 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.
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).
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
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:
🎉 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:
- RVM or rbenv
- A nice version of
- Confirmation that you’re using
Bundlerinstead 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)
.ruby-versionin 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/rvmin case of RVM.
- When using RVM, we suggest enabling rvm to be easily executed as a function
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.
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) 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
Fastfile to run gems with versions specified in
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
Fastlane, just put the code inside
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
2.112.0, you can omit the
unless command and use just the
- Team members and CI machines will always have the correct dependencies (pods and gems) for the current working state of your code.
- 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.
- 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.
- Your CI is flawless. Before each build, you just run
bundle exec pod installin the current folder and you’re always up-to-date with all gem versions, never have to worry about new
Fastlaneversions, and it fixes some changes in AppStoreConnect API compatibility.
- Now you don’t need to keep your dependencies stored in different package managers (combining
gems) which would cause a lot of confusion around your environment.
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!