Welcome back. In this video, we will finish the discussion on designing our build system with the use of makefiles. As we learned earlier, make and makefiles can give us an opportunity to simplify and improve our build process. In this day and age of technology, the need to support many architectures, compilers, or even versions of software, is pretty typical. Not only do you want a code base that can be flexible to support these variations, you want your build system to support them as well. Just like with our software design and architecture, we need to engineer good quality into our build system. We already learned some basic features of make and makefiles to design our system. Now, we will learn some more advanced features to make our build systems even better. One major advantage of variables and makefiles, is that we can create strings of files as targets to compile. This was a great feature, because these string variables allow for more dynamic code target definitions. Meaning, we could create generic rules that can apply for any file in a source list. There were two issues with this. First, we had to create two separate lists for our source files and our object files. And the second issue, is that we had to independently track our prerequisites or our dependencies for each target, creating individual targets. This caused our makefile to contain many repeated rule definitions with different file names. Make has two features to make these two issues go away. Make provides us the ability to auto-generate prerequisites. In addition, make will allow us to create an implicit rule, by using a pattern match to associate C files to object files. We do this with the use of the % operator. The % operator is used for matching file names. Therefore, if we create a pattern rule, we can have a %.o target and a %.c prerequisite. This will assume that any target that matches the pattern with a file that ends with a .o, will have a prerequisite of a .c file, before the recipe can be run. Now, with our sources variable list that we created previously, we invoke make with any target file base name in our sources list. And we append a .o extension, and make will know how to generate this. We can also create a variable to track all of these object files automatically, without directly writing down every single .o version of our source files. We can do this by writing the following variable definition for object files. Here, we are defining a variable OBJS, that is going to go through every word in the sources list, and replace any .c ending with a .o ending. This will execute through every source file listed and produce a new variable OBJS. Now we can highly automate the process of generating targets. Not every target you need, needs to be an actual file name. You may want to use some short hand to describe certain actionable items you want make to perform. For instance, instead of building main.out, our final executable, we may just want to build all of our files including the output executable, without knowing its name. So instead of main.out, we can say build all. When we do this, we need to introduce a new feature of make, called a directive. Just like in C with preprocessor directives, this make directive gives extra information to make on what to do. In this case, we need to use the .phony directive to indicate this non-file target, so that make doesn't confuse this with an actual file. You can make whatever target names you want and provide associated commands to do this. So, some suggested targets include shorthand for a build-all, a clean and maybe even a debug image. The clean is the most important, because this will be used to help remove previously compiled outputs and create a fresh build. Since our make file knows what input files we have, and what generated files we've created with the use of variables, we can use it to tell it to simply remove all of our generated files, instead of manually removing every single file individually. Let us go one step further of customizing this build system. What if you wanted to make file that could support multiple architectures in addition to multiple build targets. You could define completely different custom build targets for different compilers or architectures. Or you could have make do some conditional execution and assignment in the makefile, based on input parameters or system information. This should sound familiar, since this is similar to the compile time switches you have used before. We will implement them similarly as we did in C with compile time switches. Except, we might set some make file flags to define different variables, flags, or applications that we should use in the make file. To make this work, you need to define targets with variables. Then we can easily at compile time, switch what variables mean when make is run. You've already seen one of these in the previous video. We use simply expanded variables to probe the OS for information system architecture, the present wording directory, and the operating system type. This was done with a feature of makefiles called functions. Functions can do a variety of things like run Linux commands or perform conditional operations. A shell function runs a specific Linux command with options, and then runs the data back into a variable. Given this powerful shell command, you can create dynamic make files that query a system for information. This can include, but is not limited to, things like architecture directory and operating system. We can then combine the shell command output and use the conditional operator, ifeq, to see if we're currently executing on a Linux OS. Conditional execution is not limited to shell function output. We can actually pass parameters into make to help make decisions. For example, we can create a variable called PLATFORM. This PLATFORM variable will help us to determine which compiler we are going to use for building. If we pass in the PLATFORM flag override, and specify that the platform is to be built for an MSP432, then we need to change the target CPU to the cortex-m4. If the target platform is the Freedom board then we need to change the target CPU to the cortex-m0plus. The previous example changes the target CPU. But you can extend this idea to changing various compile time or linker time flags. For instance you may want to modify the contents of the preprocessor flags, CPPFLAGS, the CFLAGS, or the linker flags. Perhaps you may even want to create a build design for debugging. In this case, you may want to add in a compile time switch that allows for software logging, adding in debug symbols, and maybe even reduce the optimizations down to zero. CPPFLAGS and CFLAGS, was not randomly chosen. These are some special variable that make uses, behind the scenes, for certain implicit rule definitions. We will not go into details of what those are here, but by defining these special variables, make can make suggestions or well informed guesses about generating certain undefined targets. GNU make and makefiles are just one option for build generation tools. As stated before, a software engineer needs to be familiar with the build system. As it is yet another tool that we can use to build in robustness, flexibility, and consistency into a software project and a team of engineers. There are both positives and negatives to using a GNU make. But it's popularity, simplicity, and free cost make it a great application to use. In this demo, I have a simple make system with two make files and a handful of source and header files. Currently this directly is clean, so no generated files. The make files consist of my main make file, that defines all of my rules, and the second is dedicated to defining the source files I need to create a complete build. If I open the sources.mk file, you will see a variable named SRCS. This variable has three files names listed, main.c, my_file.c, and my_memory.c. I used line continuation here to make it readable. Exiting this file, we can go back to the main makefile. In the main makefile, I started the file with some documentation on what targets I have, as well as variable overrides for some compiled time switches. I support three targets here, an implicit object C file rule, a build all rule, and a clean target. The next part of this file has included my sources file, followed by many variables I've defined. These variables were designed in a way to work with the ARM cortex-m0plus CPU and the thumb architecture. At the bottom of this file is my target list. I use phony directives for the non-file targets, and I reused many previously defined variables and automatic variables, to make my rule definitions very modular. Now if I exit this file, I can run make all, and you will see that four commands will be run. That is because I have three source files that each need to be built in a final linking step, that takes the object files and generates a final executable file. I cannot run these files, since they were not designed for this native environment. They were designed for our microcontroller and its cross compiler. I can follow this up by running the make clean target. This target is another phony target that removes any auto generated files from this directory and thus cleans it. You can see all objects, executables, and map files are now gone. Last, I will show you the use of the implicit rule for object files that pattern match for C files. I can compile any single object file by running make, and then the object file of interest. Let us do this with my_memory.o. In addition, I will provide an override for the target CPU by specifying the cortex-m4. You will see that only one command is issued, and it only compiles the my_memory.o object file with a new CPU flag. Looking back at the directory, you see that there is my_memory object file and no other object files.