The first series of Learning by Reversing examines a Ruby native gem to understand how it works. Part 4 digs into the development Makefile and how it supports different activities during development.
Previously, in this series, we have had:
- Part 1 – Background to the gem we are looking at (including installing, changing and rebuilding it)
- Part 2 – How a native gem is loaded
- Part 3 – How the files get packaged so that the native extension is built during gem installation
We ended the previous part with the question of Why are there two Makefiles? and moved on simply by saying that one is the development Makefile. This post gets into the details of the development Makefile so that you can see how it holds together and what you might need to do if you want to add more things.
The Development Makefile
Let’s look at what the Makefile
looks like. The file is in some path somewhere
and this is what is in it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# Begin OS detection
ifeq ($(OS),Windows_NT) # is Windows_NT on XP, 2000, 7, Vista, 10...
OPERATING_SYSTEM := Windows
PATH_SEPARATOR := ;
else
OPERATING_SYSTEM := $(shell uname) # same as "uname -s"
PATH_SEPARATOR := :
endif
# This gives debug output in the C code and some debugger flags, useful for... Debugging.
# See ext/fast_polylines/extconf.rb
DEBUG = # 1
EXT_NAME = fast_polylines
RUBY_FLAG = -Ilib -Iext -r $(EXT_NAME)
ALL_TARGETS = $(wildcard ext/$(EXT_NAME)/*.c) $(wildcard ext/$(EXT_NAME)/*.h)
##@ Utility
FMT_TITLE='\\033[7\;1m'
FMT_PRIMARY='\\033[36m'
FMT_END='\\033[0m'
.PHONY: help
help: ## Shows this help menu.
@printf -- " FAST-POLYLINES\n"
@printf -- "---------------------------------------------------------------------------\n"
@awk ' \
BEGIN {FS = ":.*##"; printf "Usage: make ${FMT_PRIMARY}<target>${FMT_END}\n"} \
/^[a-zA-Z0-9_-]+:.*?##/ { printf " $(FMT_PRIMARY)%-30s$(FMT_END) %s\n", $$1, $$2 } \
/^##@/ { printf "\n$(FMT_TITLE) %s $(FMT_END)\n", substr($$0, 5) } \
' $(MAKEFILE_LIST)
.PHONY: console
console: ext ## Runs an irb console with fast-polylines
irb $(RUBY_FLAG)
.PHONY: test
test: ext ## Runs tests
bundle exec rspec
.PHONY: rubocop
rubocop: ## Checks ruby syntax
bundle exec rubocop
.PHONY: benchmark
benchmark: ext ## Run the benchmark
bundle exec ruby $(RUBY_FLAG) ./perf/benchmark.rb
.PHONY: publish
publish: test ## Publish to rubygems
gem build
gem push fast-polylines-*.gem
ext/$(EXT_NAME)/Makefile: ext/$(EXT_NAME)/extconf.rb
cd ext/$(EXT_NAME) && ruby extconf.rb --vendor
ext/$(EXT_NAME)/$(EXT_NAME).bundle: ext/$(EXT_NAME)/Makefile $(ALL_TARGETS)
make -C ext/$(EXT_NAME)
##@ C extension
.PHONY: ext
ext: ext/$(EXT_NAME)/$(EXT_NAME).bundle ## Compiles the C extension
.PHONY: clean
clean: ## Cleans compiled stuff
cd ext/$(EXT_NAME) && make clean
rm ext/$(EXT_NAME)/Makefile
rm fast-polylines-*.gem
This is our development Makefile and so, we expect it to help us during development. This means that we minimally want to be able to do the following without having to package and install the gem:
- Rebuild the extension and see compiler warnings and errors
- Test the extension
- Interactively try to use the extension
In this post, we will not try to only read the Makefile
and make sense of it but instead work forward from what we know we want to achieve.
Building the Native Extension
We have seen previously that the extension is built using the actual Makefile. That Makefile is generated during the gem installation by ext/extconf.rb
. After that, the need is to just do make
on that Makefile.
The picture below shows the link between the main parts related to the extension.
So, this is how it links up:
ext
(#1) is the main point and has a dependency onext/$(EXT_NAME)/$(EXT_NAME).bundle
(#2). This only requires the dependency to exist. Nothing else is needed to be done.- This has a dependency on
ext/$(EXT_NAME)/Makefile
(#3) and$(ALL_TARGETS)
(#4) – this has a rule attached to it which doesmake -C ext/$(EXT_NAME)
. If the dependency exists and is updated, it runsmake
on it. If not, the next item is run. - The
Makefile
has a dependency onext/$(EXT_NAME)/extconf.rb
and has a rule to change directory and executeruby extconf.rb --vendor
. This ensures that theMakefile
is generated by runningextconf.rb
during development. - The last bit is $(ALL_TARGETS). This is specified further up in the file as
ALL_TARGETS = $(wildcard ext/$(EXT_NAME)/*.c) $(wildcard ext/$(EXT_NAME)/*.h)
– this sets all the C and H files as dependencies. If any of them change, the bundle needs to be updated, i.e., we need to runmake
.
The picture below shows the dependencies and how they feed in to building the extension during development. Any changes to the sources layer will result in the extension being rebuilt.
Testing the Native Extension
Once you build the native extension yourself, you can test it by doing bundle exec rspec
that runs the RSpec tests for the native extension. This is what is reflected in the development Makefile
as:
1
2
3
.PHONY: test
test: ext ## Runs tests
bundle exec rspec
Running make test
will ensure that this make target runs. This specifies ext
as a dependency and will rebuild the extension if any of its dependencies have changed (as explained in the previous section).
Running rubocop
We now have a good expectation of how this would work. Once you build the native extension yourself, you would run rubocop
by doing bundle exec rubocop
. This is what is reflected in the development Makefile
as:
1
2
3
.PHONY: rubocop
rubocop: ## Checks ruby syntax
bundle exec rubocop
Running make rubocop
will ensure that this make target runs. This again specifies ext
as a dependency and that will rebuild the extension if any of its dependencies have changed.
Running the console
When you’re working with a new gem, it helps to have a console that requires the locally built gem so that you can try it out. For this, we want to run irb
and get it to recognise lib
and ext
as being on the LOAD_PATH
(by using the -I
command line argument) so that it can find the locally built gem, and then doing a require by using the -r
command line argument.
The actual command we need to do is:
irb -Ilib -Iext -r fast_polylines
We already store fast_polylines
in a variable called EXT_NAME
since we use it in a couple of other places also. We put -Ilib -Iext
and the -r(EXT_NAME)
into another variable called RUBY_FLAG
so that it can be used in multiple places (you’ll see this again in the next section) and we end up with the below lines:
EXT_NAME = fast_polylines
RUBY_FLAG = -Ilib -Iext -r $(EXT_NAME)
irb $(RUBY_FLAG)
This is then exactly what is put for the make target called console
as below. As before, ext
is specified as a dependency so that it is rebuilt if anything changes.
Running the benchmark
If you’re building a native gem for performance reasons, you will definitely want to add a benchmark to show that the new version is faster (and by how much). In the case of this gem, the performance benchmark is in a Ruby script ./perf/benchmark.rb
and needs to be run. To run it manually on the command line, you would run Ruby while requiring the newly built native gem and the running the performance script.
As in the previous section, this means running ruby
with the same RUBY_FLAG
values: -Ilib -Iext -r(EXT_NAME)
as we did for irb
for the console
command. The make target benchmark
is defined as below with ext
as a dependency as always.
Publishing the gem
If you have permission to push the gem and your environment is set up for it, you could use the gem push
command to push the gem. The Makefile publish
target does this, by setting test
as a dependency (which in turn sets ext
as a dependency), If the test
target completes successfully, it will do gem build
and gem push
for the built gem.
Cleaning up
Almost all makefiles will have a target for clean
so that you can get a clean environment by doing make clean
. This is done using:
This does the following:
- cd ext/$(EXT_NAME) && make clean => runs the
make clean
on the main Makefile - rm ext/$(EXT_NAME)/Makefile => remove the main Makefile
- rm fast-polylines-*.gem => remove the previously built gem
Getting help
The Makefile has a help
target that shows how to use the Makefile and that outputs formatted help text.
Two more things to understand
In the previous sections, we have gone through the structure of the development Makefile and hopefully, it is now clear and well understood. There are two more things that are helpful to understand.
What is .PHONY?
You might have noticed that each command started with .PHONY
as in .PHONY test
– what is .PHONY
? This refers to a Makefile concept called Phony Targets. According to the make documentation, a phony target is one that is not really the name of a file. It is the name of a recipe to be executed when you make a specific request (e.g., make clean
or make test
or make rubocop
, etc. in our case). If we did make rubocop
without using a phony target and if there was a file called rubocop
on the path, we might have a situation where make
might decide to do nothing since the file has not changed. For this reason, we state .PHONY: rubocop
which will result in make
running the recipe irrespective of whether a file called rubocop
exists since .PHONY
is a special built-in target name.
The documentation also notes that prerequisites of .PHONY are always interpreted as literal target names, never as patterns (even if they contain ‘%’ characters). So, we must use the exact literals (e.g., rubocop, test, ext, clean, help, etc.) as the prerequisites for .PHONY
.
Supporting Windows
A lot of Ruby developers work on Linux or Mac, and not at all on Windows. Although Ruby on Windows is now very good and most native extensions work very well and without any change, the issue often is a Makefile that excludes Windows support unintentionally.
In the case of this gem, there were a couple of related items that needed minor changes:
- Directory Separators
- Multiple arguments
We use -I
to pass paths to irb
and ruby
as command line options. The description for -I
says: “-I directory | Used to tell Ruby where to load the library scripts. Directory path will be added to the load-path variable (”$:“).”
There are two ways to provide multiple directories as command line options:
- You can use the
-I
option multiple times. This would be the same as doing-Ilib -Iext
to pass the two directories. This method works on both Linux, Mac and also on Windows. - The shortcut is to separate the multiple paths using a separator. On Linux/ Mac, the separator is
:
but on Windows, the separator needs to be;
– this means that-Ilib:ext
does not work on Windows and-Ilib;ext
does not work on Linux.
For this reason, it’s easiest if you use -Ilib -Iext
so that there is no issue across platforms. However, if you do want to use the shortcut, you have to do some extra work to detect the OS and adjust the separator.
After this, you can use PATH_SEPARATOR
for RUBY_FLAG
as below:
RUBY_FLAG = -I lib$(PATH_SEPARATOR)ext -r $(EXT_NAME)
However, I did find that because of the way the development Makefile is used in Windows, this method is not fool-proof since some parts of the process are run through bash
and some using the Windows command processor.
For this reason, I recommend just using multiple -I
options instead.
Summary
We have continued on our journey to stitch things together. In this post, we looked at the development Makefile and saw how it provide us targets for:
- Building the native extension
- Testing the native extension
- Running rubocop
- Running the console
- Running the benchmark
- Publishing the gem
- Cleaning up
- Getting help
We also looked at what Phony targets are in a Makefile, and also at how Windows is supported.
Looking ahead
This brings us to the end of Part 4 where we looked at the Makefile that holds everything together during development. Hopefully, you found it useful. Stay tuned for Part 5.
I will add links and references later, possibly in the last post of the series. If you have any comments, please feel free to leave them below.