The convention for writing Makefiles is to have the default command (your entry point) at the top of the file and have Make process the commands from the top down. You don’t have to do this, though (as you’ll see, I’ve not really worried about it with the examples throughout this post), and you’re free to put your rules in whatever order makes sense to you. But be aware that when you call the Make command, you’ll want to specify the specific target if it’s not the default. Terminology Link There are three key phrases you need to be aware of when talking about a Makefile: Rules Targets Prerequisites The following snippet demonstrates the basic structure of a Makefile: target: prereq1 prereq2 commands You can see we have: a single target (this is what we reference when running the commandmake ); a set of dependencies (i.e. prerequisites); and a command to execute (e.g.jshint test.js --show-non-errors). This entire structure is collectively referred to as a “rule” and a Makefile is typically made up of multiple rules. Prerequisites Link Prerequisites are the dependencies for the target. What this means is that the target cannot be built successfully without the dependencies first being resolved. Imagine we’re compiling Sass into CSS. An example Makefile (which we’ll look at in more detail shortly) could look like: compile: foo.scss sass foo.scss foo.css In the above example we specified the prerequisite as being foo.scss; meaning Make will either look for a target called foo.scss or expect a file to exist in the current directory structure. We don’t have a target named foo.scss and so if that file also didn’t exist, then we couldn’t resolve the dependency and subsequently the rule would fail (if it can’t resolve the dependency then the command in the rule won’t be executed). How Make Decides What To Do Link How and why Make decides what to do when you run make is very important as it’ll help you understand the performance implications of certain tasks. The rule of thumb for Make is pretty simple: if the target (or any of its prerequisite files) are out of date or missing, then the commands for that target will be executed. Make uses the modification timestamp to avoid duplicate processing. If the timestamp of the dependent files is older than the resulting output, then running Make won’t do anything. Hence you can force Make to recompile a file by simply using the touch command on the relevant files. Note: if you want to see what Make will execute without it actually doing anything, then run themake command as you normally would but ensure you include the -n flag. This will cause Make to print out all commands that would be executed, including commands collated from any specified prerequisites. Automatic variables Link Let’s consider another example whereby we want to compile a Sass style sheet into CSS: compile: foo.scss sass foo.scss foo.css We have some slight duplication here, the reference to foo.scss. We can clean this up a bit by using some special variables that Make provides (also referred to as automatic variables). Specifically for the problem we want to solve, we’ll be using the $< automatic variable. When the compile target is run, the $< variable will reference the first prerequisite in the list, which will simplify the example and save you from having to repeat yourself. The following example demonstrates what this looks like: compile: foo.scss sass $< foo.css This is good because we’ve removed a hardcoded value and made our code slightly more flexible. But what happens if we have multiple dependencies? Assume we have three files foo.txt, bar.txt and baz.txt. We can use a combination of the $^variable (which gives us all the dependencies/prerequisites as a list) and a small bit of standardBash shell code (Make commands are ultimately structured shell scripts with extra syntactical sugar) to loop over the provided dependency list. The following example demonstrates how this could be written: list: foo.txt bar.txt baz.txt for i in $^; do echo "Dependency: $$i"; done Executing make list would result in the following response: for i in foo.txt bar.txt baz.txt; do echo "Dependency: $i"; done Dependency: foo.txt Dependency: bar.txt Dependency: baz.txt Note: because Makefiles have their own special syntax, the use of $ will conflict when writing our shell script (which also has its own special syntax around $). This means if we want to use the dollar character and not have it be Makefile specific, then we have to escape it using another dollar. So rather than writing $i – which works fine within the context of a normal shell script – we’ve had to write $$i instead. We’ll see a few different automatic variables throughout this post, but in the meantime check out the quick reference list below for some of the more useful ones: $<: first prerequisite $^: list of prerequisites $?: list of prerequisites that have changed $@: target name $*: the value of a target placeholder The full reference of automatic variables is available on the GNU Make website. Later on in this post we’ll revisit this for loop example and demonstrate a more idiomatic way to achieve the result we want. Commands Link It’s worth being aware that each command provided inside the overall rule is considered a separate shell context. This means if you export a shell environment variable in one command, it won’t be available within the next command. Once the first command has finished, a fresh shell is spawned for the next command, and so on. You’ll also notice that when running Make it will print out the command instructions beforeexecuting them. This can be disabled in one of three ways. You can either run Make with the -s flag, which will silence any output; or you can use the @ syntax before the command itself, like so: list: foo.txt bar.txt baz.txt @for i in $^; do echo "Dependency: $$i"; done The third way to silence output is to use the .SILENCE flag. The following snippet demonstrates how to silence three targets: foo, bar and baz: .SILENT: foo bar baz Note: silencing the output unfortunately also means silencing any errors! Much like shell scripting, if you have a command that is more complicated than what can feasibly fit on a single line, then – for the sake of readability if nothing else – you’ll need to write it across multiple lines and escape the line breaks using the character, as the following example demonstrates: list: foo.txt bar.txt baz.txt for i in $^; do echo "Dependency: $$i"; done Targets As Prerequisites Link So far our prerequisites have been physical files that already existed. But what if you need to dynamically create the files first via other targets? Make allows you to specify targets as dependencies, so that’s not a problem. Let’s see how this works in the following example: foo: @echo foo > foo-file.txt bar: @echo bar > bar-file.txt baz: foo bar @echo baz | cat - foo-file.txt bar-file.txt > baz-file.txt Note: Make typically uses the convention of naming targets after the files they create. This isn’t a necessity but it’s generally considered good practice What we have are three targets: foo, bar and baz. The first two have no dependencies of their own and all they do is generate a new text file. The last target, baz, specifies the other two targets as its dependencies. So when we run make baz we should see no output (as we’ve used the special @ syntax to silence any output) but we should find we have the following files created: foo-file.txt bar-file.txt baz-file.txt The last file in the list should contain not only a line that displays baz but also two other lines comprising the contents of the other files. So running cat baz-file.txt should print: baz foo bar Note: if you’ve not seen it used before, the - in the cat command is telling it to expect input from stdin (the echo command writes to stdout and that is piped | over to the cat command as stdin) Accessing Targets Link In the above example, I was generating a file based on the contents of two other targets (which themselves dynamically generated some files). There was a slight bit of repetition that could have been cleaned up if we used another automatic variable provided by Make, specifically $@. The $@ variable is a reference to the target name, so let’s see how we can use this with our previous example: foo: @echo $@ > "$@-file.txt" bar: @echo $@ > "$@-file.txt" baz: foo bar @echo $@ | cat - foo-file.txt bar-file.txt > "$@-file.txt" In the example above we’ve saved ourselves from typing foo, bar and baz a few times but we’ve not eradicated them completely as we still have to reference foo and bar as prerequisites, as well as referencing them from within the baz command itself. With regards to the baz command, we could use $^ along with some shell scripting to clean that up so we’re again not relying on hardcoded values. The following example shows how to achieve that: foo: @echo $@ > "$@-file.txt" bar: @echo $@ > "$@-file.txt" baz: foo bar @files=$$(echo $^ | sed -E 's/([a-z]+)/1-file.txt/g'); echo $@ | cat - $$files > "$@-file.txt" Oh boy, OK. So yes, we’ve removed some more hardcoded values, but unless you’re supremely confident with shell scripting then I’m guessing the above refactor won’t make much sense to you. But let’s break it down a bit so we can see what we have: We use $^ to get the list of dependencies; in this case, foo bar. We pipe that over to the sed command. We also use the extended regular expression engine -E to make our regex pattern easier to understand. The sed command replaces foo bar with foo-file.txt bar-file.txt. We do that replacement within a subprocess $(), which is a special shell syntax. This means we have to escape the dollar sign within the Makefile ($$()). The values returned from the subprocess (foo-file.txt bar-file.txt) are then stored in a variable called files and we reference that variable in place of the original hardcoded values. On top of all that, we still have duplication: the foo and bar referenced within the prerequisites area. That has to be hardcoded unless we’re going to use Make or some other form of shell scripting to dynamically generate the actual Makefile itself; which even for me is a step too far in this case. OK, so what does this ultimately tell us? That simplicity is the key. The reason I went to all this trouble is it allowed me to demonstrate first, how to really stretch what Make can do for you if you have enough shell scripting knowledge; and second, to allow me to now demonstrate how you can use more idiomatic Make to simplify the code and avoid overengineering like the previous example: baz: foo-file.txt bar-file.txt echo $@ | cat - $^ > $@-file.txt %-file.txt: echo $* > $@ In this refactored version we define a target called baz and we set its dependencies to be two files that don’t exist. We also don’t have any defined targets in our Makefile either. To solve this problem we use a virtual rule, one that uses Make’s % placeholder syntax to pattern match against. We’ll see the % syntax in more detail shortly, but for now it will suffice to know that it acts like a wildcard. When we run make baz, Make will try to resolve the two dependencies. The following rule %-file.txt will then match both foo-file.txt and bar-file.txt and so the command echo $* > $@ will be executed twice. The command takes the dynamic part of the rule (the foo and bar parts) and makes them available via $*. We write those two values into $@, which is the target name (in this case foo-file.txt and bar-file.txt) and subsequently create those two files. We’ve now resolved the baz rule’s dependencies and we can move on to executing its command, which completes the requirements as we’ve already seen. Parsing Targets And Prerequisites Link There are many different automatic variables available for Make and we’ll see a few more of them as we go along. But as we’ve already discussed $@ and $<, it’s worth noting that you are also able to parse the specific directory and file name details for the first dependency and the target by using the syntax $() pattern. Note: because we’re using the shell function, we use := for simple expansion rather than =, which would allow for recursive dereferencing and could cause problems depending on what your Makefile and shell script is doing. In the following example we use the shell function to calculate the result of adding 1 and 1. We then dereference that value from within our target: calculation := $(shell echo $$((1 + 1))) shelled_value: @echo $(calculation) Note: in the shell, to do arithmetic (and other such things) we need to use the expression utility$((...)), so don’t make the mistake of thinking it’s a syntax special to Make, because it’s not. EVAL LINK In the following snippet we use the eval function to create a Makefile variable dynamically at runtime: dyn_eval: $(eval FOOBAR:=$(shell echo 123)) @echo $(FOOBAR) We use the shell function to return a dynamically generated value (in this case 123) and we assign that to a variable FOOBAR. But to allow us to access FOOBAR from other commands within this target, as well as other unrelated targets, we use eval to create the variable globally. Finally, we use $() to dereference the variable. FILES LINK The following technique allows us to carry out simple substitutions, by swapping the matched text before the = with the text that follows it. The defined pattern is then applied to the variable being dereferenced: files = foo.txt bar.txt baz.txt change_ext: @echo $(files:.txt=.doc) The above example produces the following output (notice how the files list of files now have.doc extensions): foo.doc bar.doc baz.doc There are many functions and techniques to help you extend the capabilities within Make and so I would highly recommend you have a read through the functions listed in the GNU Make manual. User-Defined Functions Link You’ve already seen the use of macros via the syntax define. User-defined functions work exactly the same way but you call them differently to macros (you’ll use the Make built-in callfunction), and this is so that you can pass arguments to the definition. This is best demonstrated with an example: define foo @echo "I was called with the argument:$1" endef call_foo: $(call foo, "hello!") The example above would be executed with make call_foo, and would result in the following output: I was called with the argument: hello! Note: earlier we noticed that Make would include a space when using the += operator. The same happens with function arguments and so when creating the string that is printed I didn’t include a space after the : but the output shows a space thanks to Make. You can pass as many arguments as you like to a function and it’ll be accessible numerically (e.g. $1, $2, $3 and so on). You can also call other functions from within a function and pass on the arguments, or pass different arguments using the $(call function_name) syntax. Conventions Link There are some well-known conventions and idioms used by the Make community, and a few of the most prominent ones are detailed in this section. The first is the inclusion of a clean target which should be used to remove any files created by your Makefile. This is to allow you to clean up after your tasks have executed (or if things have gone haywire). Typically the default target will specify clean as a prerequisite so as to clear your workspace before starting a fresh build. The second is to have a help target which echos each of the targets within the file and explains its purpose. As demonstrated below: help: @echo foo: does foo stuff @echo bar: does bar stuff @echo baz: does baz stuff Note: you could use some clever shell scripting along with Makefile comments to dynamically generate the printed commands and their descriptions (e.g. read in the Makefile source and parse out the meta data/comments as part of a sub shell $(shell ...)). The third is to include a reference to a special target called .PHONY at either the top or bottom of your Makefile, followed by a list of target names. The purpose of .PHONY is to prevent conflicts with files within your current project directory that coincidentally match the name of your Makefile targets. To clarify what this means in practical terms: Make has a convention whereby you would define a target’s name as matching the name of the file the commands will ultimately create; because although Make is useful for general purpose tasks, it was originally designed for creating application files. Make will associate a target with any file that matches its name and will intelligently monitor the dependencies for the target to see if it’s OK to re-execute the target’s command to regenerate the file. Typically a target such as clean won’t have any dependencies (not all the time mind you, but most of the time it won’t because the purpose of clean is to remove generated files; it shouldn’t depend on any other files in order to complete that action). If a target has no dependencies then Make will always run the associated commands. Remember, Make can intelligently avoid running certain commands if it knows the dependencies haven’t changed at all. By specifying clean as being a “phony” target, it means if there was ever a file called cleanadded to your project then we could avoid confusion as to how Make should handle running the target. The following demonstrates how it is used. It assumes you have a file – with no file extension – called clean in your main project directory: .PHONY: clean clean: @echo "I'll do something like remove all files" In the above example, running make clean will display the message “I’ll do something like remove all files”. But if you remove the .PHONY: clean and rerun the target (using make clean) you’ll now find, because we have a clean file in our main project directory and no dependencies for that target, that Make will mistakenly think there is nothing left to do and so it displays the message: make: 'clean' is up to date. Note: like with automatic variables, there are many different special targets (so far we’ve seen.PHONY and .SILENT). One that’s worth further investigation is .DELETE_ON_ERROR, which indicates to Make that if any of the commands for your target rule fails then it should delete the associated target file in your project. A list of special targets is available on the GNU Make website. Revisiting The For Loop Example Link Earlier on we looked at a way of using a for loop as a command to loop over a list of text files and to print their names. Let’s now consider two alternative ways of achieving this. The first uses a few more Make functions, while the second is more readable – but ultimately they use similar solutions. Here is the first alternative: my_list = $(addsuffix .dep, $(wildcard *.txt)) print_list: $(my_list) %.dep: % @echo "Text File:" $< The first thing we do is use the wildcard function to retrieve a list of text files (this is equivalent to $(shell ls *.txt)). We then use the addsuffix function to convert something like foo.txt into foo.txt.dep. This doesn’t actually create any files, by the way; you’ll see why we do this in a moment. Next we create a target called print_list and we set its dependencies to be themy_list list of file names (e.g. foo.txt.dep bar.txt.dep baz.txt.dep). But obviously there are no such targets defined in our Makefile so this leads us to the next step. We dynamically create targets that would match what’s found in my_list using a placeholder, and we set the dependency for these dynamic targets to be the text file itself. Remember that the target %.dep would match foo.txt.dep and so subsequently setting the dependency to just % would be the value foo.txt. From here we can now echo the file name using $<, which gives us the first dependency in the list (of which we only have one anyway). Now here is the second alternative: my_list = $(wildcard *.txt) print_list: $(my_list) .PHONY: $(my_list) $(my_list): @echo "Text File:" $@ Again, let’s take a moment to break this down so we understand how it works: Like the first alternative, we retrieve the list of files using the wildcard function. The difference now is that we don’t need to create a copy of the list and modify the names. Next we create a target called print_list and we set its dependencies to be themy_list list of file names (e.g. foo.txt bar.txt baz.txt). As we mentioned before, there are no such targets defined in our Makefile. The next step is to define a .PHONY target. We do this because in the subsequent step we define a virtual rule, but we don’t specify any prerequisites. This means as we have actual files in our directory that match the potential target name, the rule will never be executed unless we specify it as being .PHONY. Now we define our virtual rule and we use the $@ to print the name of the file when we execute make print_list. Includes Link Make allows you to import more Make specific-functionality via its include statement. If you create a file with a .mk extension then that file’s Make related code can be included in your running Makefile. The following example demonstrates how it works: include foo.mk # assuming you have a foo.mk file in your project directory included_stuff: @echo $(my_included_foo) The above example relies on a foo.mk file containing the following Make contents: my_included_foo := hi from the foo include When we run make included_stuff, we see hi from the foo include printed out. Note: the include statement can also be written with a hyphen prefix like so -include, which means if there is an error loading the specified file then that error is ignored. Conclusion Link We’ve barely even scratched the surface of what’s possible using Make, but hopefully this introduction has piqued your interest in learning more by either reading the GNU Make manual or picking up a book on the subject. I am myself only beginning my investigation into replacing my existing build tools with Make. It’s been part of my journey to rediscover original Unix tools that have stood the test of time (for good reason) rather than picking up the new shiny thing which is often nothing more than a slightly modernized abstraction built for people who want to avoid the terminal/shell environment – somewhere I’ve become much more comfortable working the past couple of years. (al, ml, rb, jb) | Cyprus Website Design | Website Design in Cyprus | Cyprus Affordable Websites | SEO & e-Marketing Services in Cyprus | Request a Web | requestaweb.com" />

Report this website

We got your feedback!

Recent posts