Making my Makefile find things for me
Tags: makefile, sdl2, windows, tutorial
Published: 2022-03-06 02:34:33

Welp, this is my first actual post on [dev.to](dev.to), and what better way to document my slow descent to madness then to dedicate my first post to some "fun" and "cool" stuff I learned while trying to set up a `Makefile` for a SDL2 game project I'm using as a learning exercise :-)

I'll make this short maybe further expand on stuff in a later post. Also, my apologies of the discussions seems a little hurried and unorganized. This is my first post and I'm writing this later into the evening. Hopefully as time goes on, I'll improve in my writing ability!

Ran into a problem of my own making when pulling the SDL2 MINGW libraries [SDL2 Dev](https://github.com/libsdl-org/SDL/releases), [SDL Image](https://github.com/libsdl-org/SDL_image/releases), and so on.  So... I decided, in my infinite wisdom, that I didn't want to go through each of the MINGW tarballs to find all the includes, libs, and DLLs. Yes, DLLs. Not only did I want to make sure that my future game code can be compiled on Linux, but that I can actually do some coding on my Windows machine. 

And I wanted my Makefile to do that all for me.

```
PLATFORM_VERSION = x86_64
EXTERNAL_SDL2_DEP=../MINGW-SDL2
ifeq ($(OS), Windows_NT)
	ifeq ("$(wildcard $(EXTERNAL_SDL2_DEP))","")
$(error "Failed to locate expected '$(EXTERNAL_SDL2_DEP)' directory containing SDL2 dependencies!")
	else
INCLUDES := $(shell powershell 'Get-ChildItem "include" -Directory -Path "$(EXTERNAL_SDL2_DEP)" -Recurse | Where {$$_.FullName -match "$(PLATFORM_VERSION)"} | Resolve-Path -Relative')
INCLUDES := $(subst \,/,$(addprefix -I, $(addsuffix /SDL2, $(INCLUDES))))
LIBRARIES := $(shell powershell 'Get-ChildItem "lib" -Directory -Path "$(EXTERNAL_SDL2_DEP)" -Recurse | Where {$$_.FullName -match "$(PLATFORM_VERSION)"} | Resolve-Path -Relative')
LIBRARIES := $(subst \,/,$(addprefix -L, $(LIBRARIES)))
SDL_FLAGS+=$(INCLUDES) $(LIBRARIES)
	endif
else
SDL_FLAGS+=$(shell sdl2-config --cflags)
endif
```

Above we can see that I'm specifying `x86_64` as my platform of choice... sorry no 32-bit architectures for me. I tossed all the SDL2 release folders I downloaded from Github to my the `MINGW-SDL2` folder that sits parallel to my game project directory.

I then check to see if the OS I'm sporting is Windows, and if so, I proceed to run some [Powershell](https://docs.microsoft.com/en-us/powershell/) commands to automatically locate the includes and libs. Using the `addprefix` and `addsuffix` Makefile functions, I construct the `-I<path/to/inc>` and `-L<path/to/lib>` parameters that the `gcc` compiler will require later on.

Minor note 1: *Usually when looking through Google and StackOverflow results on how to do certain things in Makefiles, you'll see folks point out the use of `$(shell <...>)` calls which invoke Bash/Zsh commands. Since I'm in Windows, I want to invoke Powershell commands that do similar things to their Linux counterparts and make sure those commands work when either running in the Powershell shell itself or via some other shell like the MINGW Git Bash shell.*

Minor note 2: *The use of `$$` in the `Where {$$_.DirectoryName -match "$(PLATFORM_VERSION)"}` Powershell call is important. The `$` is already used in a Makefile for referring to variables and executing function calls. In order to use the `$` for the Powershell call, you need to "escape" it with another `$`, hence the `$$`. You can find this in the [Makefile manual online](https://www.gnu.org/software/make/manual/make.html).*

To breakdown the Powershell calls a bit... 
- The [Get-ChildItem](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-childitem) call is sort of like the Bash `ls` and `find` commands smashed into a single super command.
- The `Where {...}` call (similar to [Where-Object](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/where-object)) is a simple filter on the `PLATFORM_VERSION` string `x86_64` so we ignore any `i686` directories.
- Finally the [Resolve-Path](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/resolve-path) call returns a simple list of directory paths related to the Makefile's location as a newline separate list. When stored in the `INCLUDES` variable, this result is constructed into a list of relative directory paths.

Let's look at the two lines setting up the `$(INCLUDES)` variable:
```
INCLUDES := $(shell powershell 'Get-ChildItem "include" -Directory -Path "$(EXTERNAL_SDL2_DEP)" -Recurse | Where {$$_.FullName -match "$(PLATFORM_VERSION)"} | Resolve-Path -Relative')
INCLUDES := $(subst \,/,$(addprefix -I, $(addsuffix /SDL2, $(INCLUDES))))
```
Adding additional logging calls `$(info INCLUDES: $(INCLUDES))` after each line, you can see this process more clearly.

Here is the result of the first line which located all the `include` directories for the `x86_64` platform:
```
INCLUDES: ..\MINGW-SDL2\SDL2-devel-2.0.20-mingw\SDL2-
2.0.20\x86_64-w64-mingw32\include ..\MINGW-SDL2\SDL2_image-devel-
2.0.5-mingw\SDL2_image-2.0.5\x86_64-w64-mingw32\include ..\MINGW-
SDL2\SDL2_ttf-devel-2.0.18-mingw\SDL2_ttf-2.0.18\x86_64-w64-
mingw32\include
```
Here is the result of the second line which prepends and appends `-I` and `\SDL2` respectively to each entry and converts all `\` to `/` in the above list to provide the correct linker arguments for the `gcc` compiler call later on in the Makefile:
```
INCLUDES: -I../MINGW-SDL2/SDL2-devel-2.0.20-mingw/SDL2-
2.0.20/x86_64-w64-mingw32/include/SDL2 -I../MINGW-SDL2/SDL2_image-
devel-2.0.5-mingw/SDL2_image-2.0.5/x86_64-w64-mingw32/include/SDL2 
-I../MINGW-SDL2/SDL2_ttf-devel-2.0.18-mingw/SDL2_ttf-
2.0.18/x86_64-w64-mingw32/include/SDL2
```

Minor note 1: *Setting up the compile flags for Linux is so easy with `sdl2-config --cflags` ... but doing things in Windows is always more complicated and "fun" for those insane enough to try I suppose :P*

Minor note 2: *The use of the "walrus operator :=" above is kind of important when setting the `INCLUDES` variable. Basically evaluates the right-hand and stores the results in the variable. Without it, you'd essentially store the `$(shell powershell ...)` call in the variable and get a Make error due to the `INCLUDES` variable being present in its own redefinition. You can find more on this in the [Makefile manual online](https://www.gnu.org/software/make/manual/make.html).*

As for DLLs, I do the following:
```
## DLL files and output location where they will be copied
ifeq ($(OS), Windows_NT)
DLLS_SRC  := $(shell powershell 'Get-ChildItem "*.dll" -Path "$(EXTERNAL_SDL2_DEP)" -Recurse | Where {$$_.DirectoryName -match "$(PLATFORM_VERSION)"} | Resolve-Path -Relative')
DLLS_O    := $(addprefix $(BIN_DIR)/, $(notdir $(DLLS_SRC)))
endif
...
...
## Copy all the DLLs for Windows build
$(DLLS_O):
ifeq ($(OS), Windows_NT)
	$(foreach dll,$(DLLS_SRC),$(shell powershell 'Copy-Item -Path "$(dll)" -Destination "$(BIN_DIR)"'))
endif
```

Minor note: *So it's a bit hacky. I set the `$(DLLS_O)` list to be all the DLL file names prepended with the game's bin directory path. I use this for the target. Since the Powershell `Copy-Item` command requires a comma-separated list of files when you want to copy multiple files around and the `$(DLLS_SRC)` list is space-separated, I opted to use the Makefile [foreach(..)](https://www.gnu.org/software/make/manual/html_node/Foreach-Function.html) function to copy each DLL file separately.*

Honestly, there's probably a more "elegant" way to do what I did here. But as you can see, I've used a similar Powershell command to automatically locate all the DLLs and store their relative paths in a list. Later the `$(DLLS_O)` target runs a `foreach` command that copies all these DLLs to my game project's bin directory.

Pretty insane ehh? I could have simply copied all the header (.h) files to the appropriate directory along with the library (.a and .la) files and DLL files. But noooo... I had to be simultaneously lazy and insane. But at least I learned something cool doing all this :D

Anyways, this about does it for this diary entry. For now I refuse to use tools like CMake to automatically generate the Makefile since I want to learn how Makefiles work and really get into the nitty-gritty with how things can be set up. 

There's still more interesting stuff I'm learning from manually tinkering with my project's Makefile.

See you all next time!