Friday, November 29, 2013

Sorting out Makefiles

Currently I’m playing around in C#, which involves creating a collection of files holding source code and feeding all of this code into the compiler.  The concept is pretty standard, where it gets wonky is that I bounce between the Microsoft and the Mono .Net frameworks.  It quickly became clear I needed something to help me sort out which commands to use when I bounce between environments.  Make has been working brilliantly for this.

There’s a lot of different ways you could solve this particular problem, but make was designed to handle compiling source code and seems to provide more flexibility than I’ll probably ever need.  Plus it’s available on Linux, Windows, and OS X which gives some consistency anywhere I might be working.


The syntax for makefiles gets a little cryptic, but you can create a generic file that won’t need a lot of editing to bring it to a new project.  It takes a bit to get that done, and I’m still hashing out some details on my C# makefile, but it’s really nice to have a simple way to handle the compiles.


Let’s start with a basic makefile; create text file and put this in it:

CC = C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe
TARGET = Project.exe


main : $(TARGET)
@ echo "----------------------------------------------------------"
@ echo "Compiled All Components"

%.exe : %.cs
@ echo "----------------------------------------------------------"
@ echo "Compiling $@"
$(CC) /out:$@ $<


In the first two lines we’re storing the compiler and the executable we want to build in some variables.  To reuse this makefile in other projects or environments all we should need to do is change these values.


Line four defines a target for make to attempt to resolve.  To the left of the colon is the name of this target, main.  To the right of the colon are any prerequisites for it.  The prerequisites can be other targets within the makefile or files.  Make will resolve each of those prerequisites before running the commands for this target.


You can see that our variables are marked with a dollar sign and parenthesis.  I keep them in all capitals since it makes them stand out a bit more.


Lines five and six are the commands to run to resolve this target.  Make is particular about how you indent these commands.  You must use tab characters, if you try to use spaces it will claim there is a missing separator on this line and abort.  We’re just giving it some shell commands to report what is going on.


Line eight is defining a new target, but we aren’t giving it a simple name.  What we’re saying is that any time a prerequisite is needed that has a file extension of exe handle it with this target.  The prerequisite looks similar, but it’s not a wildcard.  It’s specifically looking for a file with the same name as the target, but an extension of cs.  In our project our target is Project.exe, so when it tries to create that with this target it will only accept a source file named Project.cs.  We don’t have a target to handle files with an extension of cs, so it will expect that file to be in the current directory, if it’s not then make will quit with an error.


The last three lines are just shell commands to report what’s happening and invoke the compiler to build our executable.  The symbol $@ is a built in variable that is the name of this target, in this example it’ll be the Project.exe.  The symbol $< is a variable that holds the first prerequisite for this target, her it’ll be Project.cs.  When you substitute the values for the variables it makes that last line into:


C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /out:Project.exe Project.cs

So we've got variables that get built into the commands to compile our code. Great, we've accomplished what could be done just as easily in a shell script. Let's add in some operating system detection to this. To accomplish this we'll need two things. First the OS environment variable that Windows maintains, and second the uname command that you get on Linux and OS X.  

Make will copy all environment variables provided by the operating system into variables that you can use within your script, so we'll automatically get the OS variable. What we'll need to do is find out if it exists for that we'll need to test if that variable exists. If it doesn't then we'll want to call the uname command to find the operating system. Doing all that looks like this:

ifndef OS
OS = $(shell uname -s)
endif

The conditional statement ifndef will check to see if a variable does not exist before executing the nested commands. There's also ifdef which ensures the variable does exist before executing the code block.

Make provides a number of functions which look an awful lot like the variables, $(shell <command>) is one of these functions. Normally each line would be handled as a shell command, but the results are put on the screen. The shell function processes the supplied command the same way, but the results are instead available in the makefile script. In this case we're storing them in the OS variable.

Now we have detected the operating system, let's do something with that. We can test if values match using the ifeq conditional. Let's add that to our makefile:

TARGET = Project.exe


ifndef OS
OS = $(shell uname -s)
endif

ifeq ($(OS),Windows_NT)
CC = C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe
endif

ifeq ($(OS),Linux)
CC = dmcs
endif

ifeq ($(OS),Darwin)
CC = dmcs
endif


main : $(TARGET)
@ echo "----------------------------------------------------------"
@ echo "Compiled All Components"

%.exe : %.cs
@ echo "----------------------------------------------------------"
@ echo "Compiling $@"
$(CC) /out:$@ $&lt;

Calling these conditional statements before any of the targets makes them be handled first. You can put the various if statements within the targets, but you need to be careful with indentation. They will not work if you have a tab character in front of them. You can indent if statements with spaces though, so you can have everything indented; you'll just need to be careful how you indent each line.


No comments:

Post a Comment