Now we move onto the rbenv/src/ directory.

First file: Makefile.in.

This file is short, just 25 lines of code:

CC = @CC@

CFLAGS = @CFLAGS@
LOCAL_CFLAGS = @LOCAL_CFLAGS@
DEFS = @DEFS@
LOCAL_DEFS = @LOCAL_DEFS@

CCFLAGS = $(DEFS) $(LOCAL_DEFS) $(LOCAL_CFLAGS) $(CFLAGS)

SHOBJ_CC = @SHOBJ_CC@
SHOBJ_CFLAGS = @SHOBJ_CFLAGS@
SHOBJ_LD = @SHOBJ_LD@
SHOBJ_LDFLAGS = @SHOBJ_LDFLAGS@
SHOBJ_XLDFLAGS = @SHOBJ_XLDFLAGS@
SHOBJ_LIBS = @SHOBJ_LIBS@
SHOBJ_STATUS = @SHOBJ_STATUS@

.c.o:
	$(SHOBJ_CC) $(SHOBJ_CFLAGS) $(CCFLAGS) -c -o $@ $<

../libexec/rbenv-realpath.dylib: realpath.o
	$(SHOBJ_LD) $(SHOBJ_LDFLAGS) $(SHOBJ_XLDFLAGS) -o $@ realpath.o $(SHOBJ_LIBS)

clean:
	rm -f *.o ../libexec/*.dylib

And we already saw when we read about the configure script that the first 16 lines of this file will be replaced by variable values generated by the shobj-conf file. So let’s focus on the remaining 6 lines:

.c.o:
	$(SHOBJ_CC) $(SHOBJ_CFLAGS) $(CCFLAGS) -c -o $@ $<

../libexec/rbenv-realpath.dylib: realpath.o
	$(SHOBJ_LD) $(SHOBJ_LDFLAGS) $(SHOBJ_XLDFLAGS) -o $@ realpath.o $(SHOBJ_LIBS)

clean:
	rm -f *.o ../libexec/*.dylib

These lines aren’t affected by the sed command in the configure script- no part of them gets replaced. So these lines appear exactly the same in both Makefile.in and Makefile.

But before we get into what this syntax is, let’s look at what Makefiles are.

What is a Makefile?

The most comprehensive resource I found on Makefiles is a website called MakefileTutorial.com. It tells us that:

Makefiles are used to help decide which parts of a large program need to be recompiled. In the vast majority of cases, C or C++ files are compiled. Other languages typically have their own tools that serve a similar purpose as Make. Make can also be used beyond compilation too, when you need a series of instructions to run depending on what files have changed.

It goes on to talk about dependency graphs, and how a Makefile can be used to construct a dependency graph which tells your computer to recompile the dependencies (and the things which depend on them) if any of them change.

Additionally, according to OpenSource.com:

If you want to run or update a task when certain files are updated, the make utility can come in handy. The make utility requires a file, Makefile (or makefile), which defines set of tasks to be executed.

So the program which reads your Makefile and re-compiles your dependencies is called make.

Let’s create a simple Makefile as an experiment, following along with the first example from the above MakefileTutorial.com link.

Experiment- building a trivial Makefile example

I create a new file named Makefile and, inside it, copy/paste the following directly from the above link:

hello:
	echo "Hello, World"
	echo "This line will always print, because the file hello does not exist."

Then I run make hello in the same directory as the Makefile. I see the following:

$ make

echo "Hello, World"
Hello, World
echo "This line will always print, because the file hello does not exist."
This line will always print, because the file hello does not exist.

So far, so good.

The tutorial tells us what the different parts of our Makefile are:

  • hello:- the target of the file.
  • The two echo statements- the commands of the file.
  • No prerequisites are listed here (but we’ll see some later).

The tutorial also says the following:

As long as the hello file does not exist, the commands will run. If hello does exist, no commands will run.

It’s important to realize that I’m talking about hello as both a target and a file. That’s because the two are directly tied together. Typically, when a target is run (aka when the commands of a target are run), the commands will create a file with the same name as the target. In this case, the hello target does not create the hello file.

So by convention, if we have a target named foobar, the expectation is that the target’s commands will result in the creation of a file with the same name, i.e. foobar. But importantly, if that file already exists, the commands won’t be run.

To test this, I create an empty file named hello, and re-run make hello:

$ touch hello

$ make hello

make: `hello' is up to date.

This time, we don’t see the two echo statements, or their output, in the terminal.

The RBENV Makefile

Let’s now look at the code inside the RBENV version of Makefile. We learned earlier that this file is generated when reading the configure script. A word of warning: I’m not an expert on C, or the gcc compiler for C. And I don’t intend to become one over the course of reading this file. That would require a huge amount of effort that I’m not prepared to invest, for now. So in contrast to most other files I’ve read as part of understanding RBENV, my goal here is to learn just enough to understand how the Makefile fits in to the bigger RBENV picture.

With that said, the Makefile looks like this:

CC = gcc

CFLAGS =
LOCAL_CFLAGS =
DEFS =
LOCAL_DEFS =

CCFLAGS = $(DEFS) $(LOCAL_DEFS) $(LOCAL_CFLAGS) $(CFLAGS)

SHOBJ_CC = gcc
SHOBJ_CFLAGS = -fno-common
SHOBJ_LD = ${CC}
SHOBJ_LDFLAGS = -dynamiclib -dynamic -undefined dynamic_lookup
SHOBJ_XLDFLAGS =
SHOBJ_LIBS =
SHOBJ_STATUS = supported

.c.o:
	@echo "1st command: $(SHOBJ_CC) $(SHOBJ_CFLAGS) $(CCFLAGS) -c -o $@ $<"
	$(SHOBJ_CC) $(SHOBJ_CFLAGS) $(CCFLAGS) -c -o $@ $<

../libexec/rbenv-realpath.dylib: realpath.o
	@echo "2nd command: $(SHOBJ_LD) $(SHOBJ_LDFLAGS) $(SHOBJ_XLDFLAGS) -o $@ realpath.o $(SHOBJ_LIBS)"
	$(SHOBJ_LD) $(SHOBJ_LDFLAGS) $(SHOBJ_XLDFLAGS) -o $@ realpath.o $(SHOBJ_LIBS)

clean:
	rm -f *.o ../libexec/*.dylib

Let’s start with the first rule, since everything before it is just variable declarations.

The .c.o rule

.c.o:

What is the .c.o syntax? If we Google “.c.o Makefile”, we see this StackOverflow post, which tells us that:

It’s an old-fashioned suffix rule. The more up-to-date way to do it is to use a pattern rule:

%.o : %.c

This looks more familiar, i.e. we have two sides of a rule, separated by a colon. According to MakefileTutorial.com, the % symbol is called a “wildcard”. Here we’re using it to state that, for each file with a .c extension, we want to make an identical target with a .o extension.

What is the commend that this rule executes?

First Makefile rule- building our object file

$(SHOBJ_CC) $(SHOBJ_CFLAGS) $(CCFLAGS) -c -o $@ $<

This command takes advantage of the variables that we declare at the top of the Makefile:

SHOBJ_CC = gcc
SHOBJ_CFLAGS = -fno-common
DEFS =
LOCAL_DEFS =
LOCAL_CFLAGS =
CFLAGS =
CCFLAGS = $(DEFS) $(LOCAL_DEFS) $(LOCAL_CFLAGS) $(CFLAGS)

These variable values are used to construct the final command that we run. For simplicity, we can echo the command as a string, to see what it evaluates to.

@echo "1st command: $(SHOBJ_CC) $(SHOBJ_CFLAGS) $(CCFLAGS) -c -o $@ $<"
$(SHOBJ_CC) $(SHOBJ_CFLAGS) $(CCFLAGS) -c -o $@ $<

We prefix our echo command with a @ symbol to tell make not to print the echo command itself, just to execute it.

When we run make, we get:

$ make

1st command: gcc -fno-common     -c -o realpath.o realpath.c
...

So $(SHOBJ_CC) $(SHOBJ_CFLAGS) $(CCFLAGS) -c -o $@ $< evaluates to:

gcc -fno-common     -c -o realpath.o realpath.c

Let’s break this down:

The -fno-common flag

According to the gcc docs, the -fno-common flag tells gcc what to do if it finds multiple definitions for the same global variable. This is the default behavior for gcc, but here we’re being explicit about the behavior. By passing this flag, we’re telling gcc that, if the same global variable is defined more than once, to raise a multiple-definition error so we can investigate and fix the error.

The -c flag

According to gcc --help, the -c flag tells gcc to “Only run preprocess, compile, and assemble steps”. In other words, we’re telling gcc to only create object files, or the individual compiled files that the gcc linker later combines into an executable file. It’s unclear to me why we want to do this. I know the next step in the Makefile is to take the object file and turn it into the ../libexec/rbenv-realpath.dylib file, but I’m not sure why a regular, non-dylib file is insufficient for RBENV’s purposes.

The -o flag

Again according to gcc --help, the -o flag does the following:

-o <file>               Write output to <file>

So we’re specifying that we want our output file to be realpath.o. It’s unclear to me why we need to specify that the output object file is named realpath.o, since when I leave off the -o realpath.o flag, I still see that realpath.o is the default output filename when passing the -c flag (i.e. the default filename appears to be the same as that of the input file, but with a .o file extension instead of .c).

At any rate, the output of this first Makefile rule is realpath.o. When this file is generated, make sees that its timestamp is newer than that of ../libexec/rbenv-realpath.dylib (or if ../libexec/rbenv-realpath.dylib does not yet exist), then it will execute the 2nd rule in the Makefile.

2nd Makefile rule- building our .dylib file

The next rule in the Makefile is:

../libexec/rbenv-realpath.dylib: realpath.o
	$(SHOBJ_LD) $(SHOBJ_LDFLAGS) $(SHOBJ_XLDFLAGS) -o $@ realpath.o $(SHOBJ_LIBS)

Let’s break this up into pieces.

The rule’s target: ../libexec/rbenv-realpath.dylib

This rule’s job is to build a file named rbenv-realpath.dylib, which lives in ../libexec/ (aka the directory containing all our RBENV commands).

This is the file which the rbenv command uses here (and other commands use in a similar way) to speed up the realpath command.

The rule’s dependency: realpath.o

This is the file which the rule depends on in order to build the .dylib file. It is the output file which we just generated in the previous rule.

The rule’s command

The command that this rule executes is:

$(SHOBJ_LD) $(SHOBJ_LDFLAGS) $(SHOBJ_XLDFLAGS) -o $@ realpath.o $(SHOBJ_LIBS)

So that we can see what the variables in this rule resolve to, let’s once again print the command by adding a @echo statement into the rule:

../libexec/rbenv-realpath.dylib: realpath.o
	@echo "2nd command: $(SHOBJ_LD) $(SHOBJ_LDFLAGS) $(SHOBJ_XLDFLAGS) -o $@ realpath.o $(SHOBJ_LIBS)"
	$(SHOBJ_LD) $(SHOBJ_LDFLAGS) $(SHOBJ_XLDFLAGS) -o $@ realpath.o $(SHOBJ_LIBS)

When we delete any realpath.o that was generated from previous make runs and then re-run make, we see:

$ make

1st command: gcc -fno-common     -c -o realpath.o realpath.c
gcc -fno-common     -c -o realpath.o realpath.c
2nd command: gcc -dynamiclib -dynamic -undefined dynamic_lookup  -o ../libexec/rbenv-realpath.dylib realpath.o
gcc -dynamiclib -dynamic -undefined dynamic_lookup  -o ../libexec/rbenv-realpath.dylib realpath.o

We can see from the line which starts with 2nd command: that the command resolves to:

gcc -dynamiclib -dynamic -undefined dynamic_lookup  -o ../libexec/rbenv-realpath.dylib realpath.o

The -dynamiclib and -dynamic flags

There are certain flags and options that we can pass to gcc which are specific to Darwin, the core UNIX operating system of macOS. Since I’m running make on a Macbook, the env var SHOBJ_LDFLAGS resolves to these Darwin-specific options, thanks to the shobj-conf file which the configure script ran.

The first flag is -dynamiclib. If we Google around for this flag, we find the docs for those Darwin-specific flags here. The entry for -dynamiclib looks like so:

-dynamiclib

  When passed this option, GCC produces a dynamic library instead of an executable when linking, using the Darwin libtool command.

We don’t want to produce an executable because we’re not planning on executing the file directly. Instead, we’re passing the file to enable -f, which expects a dynamic library file as input and will load that input file dynamically, i.e. at runtime (as opposed to ahead of time, during compilation).

Similarly, the Darwin docs state the following about this flag:

OPTIONS

   Options that control the kind of output

  ...

  -dynamic

    The default.  Implied by -dynamiclib, -bundle, or -execute

This appears to be similar to the -dynamiclib flag, in that it tells gcc to output a dynamic library instead of an executable. Not sure why we need both flags, as opposed to just one or the other.

The -undefined dynamic_lookup flag

The same Darwin docs state the following:

-undefined <treatment>

  Specifies how undefined symbols are to be treated.
  Options are: error, warning, suppress, or dynamic_lookup.  The default is error.

This means that, if gcc encounters any symbols it doesn’t recognize, it should attempt to look them up at runtime, since by then they may be defined by the time we get to that point.

3rd Makefile rule- clean

The last of the rules in this Makefile is:

clean:
	rm -f *.o ../libexec/*.dylib

Some Makefiles specify an optional cleanup rule, which can be run via the make clean command. In this case, the command that we’re running is:

rm -f *.o ../libexec/*.dylib

This uses the rm command to delete all files ending in .o, as well as any .dylib (aka dynamic library) files that we created in previous rules. The -f flag says we should perform the deletion without first prompting for confirmation, regardless of the permissions on the files we delete.

That’s it for the Makefile. Let’s move on.