Makefile

From dreamcast.wiki
Jump to navigation Jump to search

Makefiles are scripts that run through a program called Make, which we use to automate and construct the building process.

Explaining compilation

First, you need to understand how source codes are turned into an executable program. Our program begins as a text file called a source code, which usually has an extension that denotes the type of file it is. Source codes for C programs tend to have a .c file extension, and source codes for C++ programs tend to have a .cpp extension. There are also .s files, which tend to be assembly code, and .h files, which are headers.

We use a program called a compiler to turn each source code file into an object file, which has a .o extension. If we have a source code called Main.cpp, our compiler will turn that into Game.o. There are many C and C++ compilers out there. In Windows, the most popular is Microsoft’s own Visual C compiler, or MSVC. There is also Clang, which is a popular cross platform compiler. Linux is actually built around a compiler at it’s heart called gcc, or the gnu cross-compiler. Gcc is built to let you take a source code and turn it into a binary object for any type of processor or platform. Our Dreamcast uses an SH4 CPU, so we need an SH4 binary (which usually has a .elf extension). We are probably writing this on an X86-64 CPU, so we need a compiler that is cross platform. Throughout this tutorial, we will be using the GNU tool set extensively, and our compiler of choice is gcc. When installing KOS, you should have installed sh-elf-gcc, which lets us create SH4 Binary elf files.

If we have multiple source files, then we’ll wind up with multiple objects. For example, if we have Main.cpp, and Library.cpp, when we compile them, we’ll wind up with Game.o and Library.o. These objects are known as translation units, and you can think of each of them as their own tiny programs. They will probably communicate with each other in our program, so they need a way to find and communicate. To accomplish this, we link them together into an elf container. In linux, we use a program called ld, which is part of gcc, to link objects into an elf.

So, what we need to do, is have a script that gathers up our source files, compiles them each into object files, then links them together into an elf. It would also be nice if our script could automate some other tasks for us. For example, it’s common on the Dreamcast to use a Romdisk image to easily manage loading data. Turning a folder into a rom disk is a multi-step command-line process normally, but we can automate it and turn it into a single command in our Makefile. We could also create commands that would launch our game for us, sending it automatically to the Dreamcast over our Serial or IP dc-load programs, and configuring our /pc/ folder. All of these tasks can be bundled into a Makefile.

Makefiles, rules, and prerequisites

The basic gist of a Makefile is that it contains small recipes which tell it how to create a file. These recipes have a list of required ingredients which must be created first before that recipe can run. Each of those ingredients needs their own recipe to build, so that, if they don’t exist, the Makefile can create them. Let’s take a look at a very simple example:

Program.elf: Game.o library.o
	gcc -o Program.elf Game.o library.o

This is a recipe to make Program.elf. We begin our definition of the recipe with a label, in this case “Program.elf” followed by a colon. To the right of this are our requirements, we need Game.o to exist, and library.o to exist. Below is the command that will be run if those requirements are met, which is to tell gcc to link the objects into a file called Program.elf. This recipe is a linker step to take our compiled filed and package them into the executable elf.

If this Makefile was in the same folder as example.c and library.c, we could call it by opening a command line to this folder, and typing “make Program.elf”. The argument after make is the recipe we want to run.

However, we would get an error if we tried this right now, it would tell us that there was no rule for “Game.o”. This is because we have no recipe for it in our Makefile. We can add one like so:

Game.o: Main.cpp
	kos-c++ -c Main.cpp -o Game.o

This is a recipe that tells us h ow to make Game.o. It requires that Main.cpp exist in the same folder. As you recall, in this hypothetical, Main.cpp exists. It runs the command to compile Main.cpp into Game.o.

We would need another rule for library.o in the same way. However, a major advantage of Makefiles is that they are programmable. You can create variables, assign them values, even do look ups or modification to lists of values. You can use wild card symbols as temporary variables in recipes and scripts, too. Let’s create a recipe that is universal for all cpp files:

%.o: %.cpp
	kos-c++ -c $< -o $@

This is a recipe template. Whenever our Makefile needs a recipe for something matching this pattern, it can generate one using this template. % is a wildcard variable that is the same everywhere it’s used. In this case, it’s saying if we need SomeFile.o, then SomeFile.cpp needs to exist. For example: Library.o needs Library.cpp, main.o needs main.cpp, other.o needs other.cpp, and so forth. Below, the command calls our compiler, but uses some strange symbols. $< refers to the first prerequisite of our recipe. If there were multiple prerequisites, we could refer to them all with $^. The symbol $@ refers to the recipe label.

These are a few other special symbols that you can use:

$? - This refers to all prerequisite files which have been changed since last compiled
$+ - Lists all prerequisites like $^, but will also include duplicates in a list
$* - List the stem root of a filename, i.e. library.o becomes libray. It strips the extension off

Makefile Variables

These symbols beginning with $ are a special type of command in our Makefile, they tell it to look up the next symbol as though it were a variable. We can define our own variables in Makefiles, too. To do so, we just start using the variable, like so:

MyVar = HELLO_WORLD

This creates a variable called MyVar. If we were to call it using $(MyVar), it would be the same as typing HELLO_WORLD. We can append it by using the += operator:

MyVar += _AGAIN!

After appending it, if we called it using $(MyVar), it would be the same as typing HELLO_WORLD_AGAIN!

We can reset it, by simply using the = operator once more

MyVar = HELLO

Calling it now with $(MyVar) is the same as typing HELLO. A Variable can also be a list of values, not a single one. Like so:

MyVar = HELLO WORLD AGAIN

MyVar is now 3 variables. Calling $(MyVar) is the same as typing “HELLO WORLD AGAIN,” but Make has a number of commands built in which can treat lists in special ways. For now, it is best to keep a separate file of rules that we can include in our Makefile. This type of organization helps us maintain a clean project. Below is an example of my Makefile.rules:

Example Makefile.rules

# Build rules
ifndef KOS_DEPDIR
%.o: %.c
	kos-cc -g $(CFLAGS) $(CPPFLAGS) -c $< -o $@
%.o: %.cc
	kos-c++ -g $(CXXFLAGS) $(CPPFLAGS) -c $< - $@
%.o: %.cpp
	kos-c++ -g $(CXXFLAGS) $(CPPFLAGS) -c $< -o $@
%.o: %.m
	kos-cc -g $(CFLAGS) $(CPPFLAGS) -c $< -o $@
%.o: %.mm
	kos-c++ -g $(CFLAGS) $(CXXFLAGS) $(CPPFLAGS) -c $< -o $@
else
%.o: %.c
	kos-cc -g $(CFLAGS) $(CPPFLAGS) -c $< -o $@ -MD -MF $(KOS_DEPDIR)/$(patsubst %.c,%.md,$(subst /,__,$(subst ..,,$<)))
%.o: %.cc
	kos-c++ -g $(CXXFLAGS) $(CPPFLAGS) -c $< -o $@ -MD -MF $(KOS_DEPDIR)/$(patsubst %.c,%.md,$(subst /,__,$(subst ..,,$<)))
%.o: %.cpp
	kos-c++ -g $(CXXFLAGS) $(CPPFLAGS) -c $< -o $@ -MD -MF $(KOS_DEPDIR)/$(patsubst %.c,%.md,$(subst /,__,$(subst ..,,$<)))
%.o: %.m
	kos-cc -g $(CFLAGS) $(CPPFLAGS) -c $< -o $@ -MD -MF $(KOS_DEPDIR)/$(patsubst %.m,%.md,$(subst /,__,$(subst ..,,$<)))
%.o: %.mm
	kos-c++ -g $(CFLAGS) $(CXXFLAGS) $(CPPFLAGS) -c $< -o $@ -MD -MF $(KOS_DEPDIR)/$(patsubst %.mm,%.md,$(subst /,__,$(subst ..,,$<)))

-include $(wildcard $(KOS_DEPDIR)/*.md)
endif
%.o: %.s
	kos-as -g $< -o $@
%.o: %.S
	kos-cc -g -c $< -o $@
%.bin: %.elf
	kos-objcopy -O binary $< $@

subdirs: $(patsubst %, _dir_%, $(SUBDIRS))
$(patsubst %, _dir_%, $(SUBDIRS)):
	$(MAKE) -C $(patsubst _dir_%, %, $@)

clean_subdirs: $(patsubst %, _clean_dir_%, $(SUBDIRS))
$(patsubst %, _clean_dir_%, $(SUBDIRS)):
	$(MAKE) -C $(patsubst _clean_dir_%, %, $@) clean

# Define KOS_ROMDISK_DIR in your Makefile if you want these two handy rules.
ifdef KOS_ROMDISK_DIR
romdisk.img:
	$(KOS_GENROMFS) -f romdisk.img -d $(KOS_ROMDISK_DIR) -v -x .svn -x .keepme
romdisk.o: romdisk.img
	$(KOS_BASE)/utils/bin2c/bin2c romdisk.img romdisk_tmp.c romdisk
	$(KOS_CC) -g $(KOS_CFLAGS) -o romdisk_tmp.o -c romdisk_tmp.c
	$(KOS_CC) -g -o romdisk.o -r romdisk_tmp.o $(KOS_LIB_PATHS) -Wl,--whole-archive -lromdiskbase
	rm romdisk_tmp.c romdisk_tmp.o
endif

define KOS_GCCVER_MIN_CHECK
$(shell \
	awk 'BEGIN { \
		split("$(1)", min, "."); \
		split("$(KOS_GCCVER)", cur, "."); \
		if (cur[1] > min[1] || \
			(cur[1] == min[1] && cur[2] > min[2]) || \
			(cur[1] == min[1] && cur[2] == min[2] && cur[3] >= min[3])) { \
			print 1; \
		} else { \
			print 0; \
		} \
	}')
endef

define KOS_GCCVER_MIN_WARNING
@echo "Skipping $(TARGET) build as current GCC version ($(KOS_GCCVER)) is less than $(KOS_GCCVER_MIN)."
endef

Creating Dreamcast Makefile

Now that we have Makefile.rules created, we can go back and start building out our Makefile. Let’s begin by including some project variables at the top of our Makefile:

include Makefile.rules
TARGET = example.elf
OBJS = Game.o
KOS_ROMDISK_DIR = romdisk
KOS_LOCAL_CFLAGS = -I$(KOS_BASE)/addons/zlib

Our first line is the include directive. It works like #include in C and C++, meaning a preprocessor copies it into this makefile when run. This is how we add our rules to our makefile. Next, we use the name TARGET to refer to the output file we want to create throughout our Makefile. OBJS is a list of our source files that we want to compile. If we create a new file, we just have to add it to the OBJS list with a .o extension and it’ll be included in our project. KOS_ROMDISK_DIR is a variable we set to let us select which directory to build our romdisk from.

Finally, KOS_LOCAL_CFLAGS is a variable name that is used in many parts of KOS. It is the flags that are added when you compile a C project. In this case, we are telling it we want to add an include definition. This begins with -I and tells our compiler to look in that folder when trying to find files. You’ll see that our folder name includes $(KOS_BASE). This is another variable set by KOS. These variables are called Environment Variables, and are set by the environ.sh file from KOS. Those Environment Variables persist throughout your session.

Let’s start with our first rule: all. This will let us be able to call “make all” to create everything all at once:

all: rm-elf $(TARGET)

This is a way we can call a series of commands in order. In this case, the prerequisites are to call the rule rm-elf first, then call the rule to create example.elf, which we refer to as $(TARGET).

Our next rule is clean. Typically, we have a clean command to remove compiled objects and other temporary files. This lets us clean everything up and start from scratch with a new compilation, which might be necessary from time to time:

clean: rm-elf
	-rm -f $(OBJS)

This calls the rm-elf rule first, then executes a command to remove the list of files in $(OBJS), i.e. Game.o.

Next, let’s define rm-elf which is being used multiple times already:

rm-elf:
	-rm -f $(TARGET) romdisk.*

This rule deletes our elf if it exists, as well as the romdisk files. Next we’ll define some rules to build specific objects:

$(TARGET): $(OBJS)
	kos-c++ -g -o $(TARGET) $(OBJS) -lpng -lz

This is our linking step, it builds example.elf. It requires all our objects be created, then links them into example.elf. The commands at the end are libraries which we are statically linking. These exist as shared object libraries in our libraries folder in linux. The convention for linking a library is that the library name should be libXXX.so, and to link it you shorten lib to just l and drop the so, i.e. -lXXX. So, if we want to link the png library, which is libpng.so, then we need to tell our linker to -lpng. Similarly, -lz is telling our linker to link libz.so. Libraries are basically the same thing as our translation unit objects that are rolled into our binary elf container, they are just packaged in a way for the system to easily refer to them.

Our makefile should have a run command, which called dc-tool and sends our elf over. You can have multiple run commands if you switch between dc-tool-ip and dc-tool-ser, but I just use one:

run: $(TARGET)
	/opt/toolchains/dc/bin/dc-tool-ip -g -t 192.168.0.009 -x $(TARGET)

This launches dc-tool-ip, sets the gdb debug flag, is given the IP of my dreamcast, then told to -x excute my example.elf file, which is referred to as $(TARGET). It requires $(TARGET) be built in the first place. If I were to run this command right now, with nothing else run in my folder, then it would build $(TARGET), by compiling $(OBJS) using gcc, then link the $(OBJS) into $(TARGET), then call dc-load-ip and send this over to 192.168.0.009. I could call this command just by running “make run” from the folder in my command line.