Maintaining workspace integrity with Makefile for react-native project

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

"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…

Makefile to the rescue!

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

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

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:

  1. 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.
  2. 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

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).PHONY: bundle-config
bundle-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:

  1. add a dependency to gems manifest
  2. 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

  1. “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

Recap

Use make clean to remove all dependencies.

Sample project with Makefile is available here https://github.com/maxkomarychev/react-native-makefile-integrity