Maintaining workspace integrity with Makefile for react-native project
By: Max Komarychev
Tags: NPM, Make, Cocoapods, React Native, Ruby

A team working with react-native project typically needs to install various tools from rather different worlds: npm packages, ruby gems and cocoapods.

In a fast-pacing project or when team is large it’s very easy for someone to forget to do yarn install or (cd ios; pod install) and end up having broken workspace.

Below I’ll show how can this be simplified with make .

TL;DR: sample project with Makefile : https://github.com/maxkomarychev/react-native-makefile-integrity

Naïve solution

We can create a script in package.json :

"deps": "yarn install && (cd ios; pod install)

And developers are supposed to run yarn deps every time they switch branch or add/change a dependency.

This will work fine as long as project is small and these operations are cheap, while on a larger project trying to reinstall dependencies may take a minute or two (simply due to sheer number of dependencies). Another downside is that it will not help managing version of cocoapods CLI itself. If two developers have different version of cocoapods they will modify Podfile.lock for no reason.

What if…

What if we can make a smart decision whether a developer needs to run yarn install followed by (cd ios; pod install) or even update his version of pod command?

Makefile to the rescue!

Quote from GNU make’s website.

GNU Make is a tool which controls the generation of executables and other non-source files of a program from the program’s source files. Make gets its knowledge of how to build your program from a file called the makefile, which lists each of the non-source files and how to compute it from other files. When you write a program, you should write a makefile for it, so that it is possible to use Make to build and install the program.

make CLI command consumes a file named Makefile which contains a set or targets and recipes to produce files.

In short a syntax looks like this:

target-file: dependency-file

<commands that create "target-file">

If dependency-file exists and its modification timestamp is higher than one of target-file commands will be executed to create new version of target-file. How can this help with dependencies?

Let’s take a look at npm packages

Developer specifies desired dependencies in a file called package.json . Upon successful installation with command yarn install a file called yarn.lock is created or updated and relevant files appear in folder node_modules .

We can create the following makefile recipe to install node_modules whenever package.lock or yarn.lock change:

YARN_LOCK_MANIFEST := $(abspath node_modules/yarn.lock) $(YARN_LOCK_MANIFEST): package.json yarn.lock

yarn install && cp yarn.lock $(YARN_LOCK_MANIFEST) .PHONY: node

node: $(YARN_LOCK_MANIFEST)

Now when you run make node a command yarn install will be executed if and only if modification time of package.json and/or yarn.lock are higher than of manifest file (I am using a copy of yarn.lock inside node_modules as a sentinel file for whole process. More about sentinel files explained here: Your Makefiles are wrong)

Cocoapods

Cocoapods are installed using 2 files: ios/Podfile and ios/Podfile.lock . As a result of running pod install a folder called ios/Pods is created which contains nested folders per every pod installed end ios/Pods/Manifest.lock file which is essentially a copy of ios/Podfile.lock and is used by cocoapods CLI itself in order to determine if pods installation is fresh.

The following Makefile can help maintaining consistency of cocapods installation:

PODFILE_LOCK_MANIFEST := $(abspath ios/Pods/Manifest.lock) $(PODFILE_LOCK_MANIFEST): $(YARN_LOCK_MANIFEST) ios/Podfile ios/Podfile.lock

cd ios && pod install --repo-update && touch $(PODFILE_LOCK_MANIFEST) .PHONY: pods

pods: $(PODFILE_LOCK_MANIFEST)

Now you can just run make pods and you can be sure your pods will be up to date.

Few things to note here:

Since many react-native libraries bring native code as pod inside node_modules we want to make sure to run pod install every time manifest file for npm packages is updated. Since cocoapods create manifest file themselves we will not copy Podfile.lock but only touch existing file to ensure its modification time is always updated in the end of the process

Different versions of `pod` CLI

ios/Podfile.lock depends on version of pod CLI command. We want to make sure every developer in our team has exactly the same version of cocoapods. Bundler gem can help here.

Let’s install bundler globally with: gem install bundler . Create initial Gemfile with bundle init .

Now add desired gems you want every developer to have:

gem "cocoapods", "~> 1.10"

gem "fastlane", "~> 2.170"

Add this to Makefile :

GEMS_MANIFEST := $(abspath vendor/Gemfile.lock)

bundle-config:

.PHONY: bundle-configbundle-config: @bundle config --local path vendor $(GEMS_MANIFEST): Gemfile Gemfile.lock | bundle-config

bundle install && cp Gemfile.lock $(GEMS_MANIFEST) .PHONY: gems

gems: $(GEMS_MANIFEST)

Now when you run make gems you will find 2 new folders in the project: .bundle and vendor : just add them to .gitignore

# gems

.bundle

vendor

Since now cocoapods depend on installed gems we want to change rules for cocoapods in the following way:

add a dependency to gems manifest run pod install as bundle exec pod install

$(PODFILE_LOCK_MANIFEST): $(YARN_LOCK_MANIFEST) $(GEMS_MANIFEST) ios/Podfile ios/Podfile.lock

cd ios && bundle exec pod install --repo-update && touch $(PODFILE_LOCK_MANIFEST)

Let’s polish

Let’s add few more helper commands to simplify usage:

“umbrella” target to install all dependencies:

.PHONY: deps

deps: node pods gems

now simply run make deps and this will make a decision which dependencies must be reinstalled.

2. a clean target to clean up dependencies if needed:

.PHONY: clean/gems

clean/gems:

rm -rf .bundle vendor .PHONY: clean/node

clean/node:

rm -rf node_modules .PHONY: clean/pods

clean/pods:

rm -rf ios/Pods .PHONY: clean

clean: clean/gems clean/node clean/pods

Now after switching a branch developer just needs to run make deps .

One last thing

Since proposed solution creates certain files which are not normally created with regular yarn or pod commands it is required to only use make way of installing dependencies to achieve maximum efficiency.

Recap

Use make deps on daily basis when switching between branches.

Use make clean to remove all dependencies.