Learn Makefiles by comparison with Visual Studio
Preface
Introduction
Under the hood of Visual Studio
GNU/Linux Equivalent
Visual Studio to Make Utility mapping
Example
Source Structure
Build
Run
Makefile
Details
Targets
Dependencies
Contents
NMake
Conclusion
External Links
Preface
If you develop software only on Windows using Visual studio, it’s a luxury. Enjoy it while it lasts. Sooner than later, you will come across Makefiles, maybe exploring some software on Linux or the misfortune of having a build system that uses make
with Cygwin on Windows.
Now you figure out that Makefiles are text files and open it in an editor hoping to get some insight into its workings. But, what do you see? Lots of cryptic hard to understand syntax and expressions. So, where do you start? Internet searches for make
and Makefiles
provide a lot of information but under the assumption that you come from a non-IDE Unix/Linux development environment. Pampered Visual Studio developers are never the target audience.
Here I will try to relate to the Visual Studio build system which will hopefully give an easier understanding of Makefiles. The goal is not to provide yet another tutorial on makefiles (because there are plenty available on the internet) but to instill the concept by comparison.
Introduction
Visual Studio provides features that are taken for granted until you have to read/create a classic Makefile
. For example, Visual Studio auto-magically does the following.
- Compiles all the sources in the project file
- Create an output directory and puts all the intermediate object files in it
- Manages dependencies between the source and object files
- Manages dependencies between the object files and binaries
- Links the object files and external dependent libraries to create binaries
All of the above have to be explicitly specified in a Makefile
. The make
utility in some ways is the equivalent of Visual Studio devenv.exe
(without the fancy GUI).
Under the hood of Visual Studio
Visual Studio is essentially a GUI over the compilation and link process. It utilizes an underlying command line compiler cl.exe
and linker link.exe
. Additionally, it provides a source code editor, debugger and other development tools.
c:\tmp> cl.exe Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86 Copyright (C) Microsoft Corporation. All rights reserved. usage: cl [ option... ] filename... [ /link linkoption... ]
c:\tmp> link.exe Microsoft (R) Incremental Linker Version 10.00.30319.01 Copyright (C) Microsoft Corporation. All rights reserved. usage: LINK [options] [files] [@commandfile]
A simple win32 console application project in Visual Studio is shown below. You have a solution
file which contains a project
file.
Invoking a build on the solution in Visual Studio calls something like the following under the hood. Yes, it looks ugly! But that is the the project properties translated to compiler/linker flags and options.
C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\CL.exe /c /ZI /nologo /W3 /WX- /Od /Oy- /D WIN32 /D _DEBUG /D _CONSOLE /D _UNICODE /D UNICODE /Gm /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Fo"Debug\\" /Fd"Debug\vc100.pdb" /Gd /TP /analyze- /errorReport:prompt Test.cpp C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\link.exe /ERRORREPORT:PROMPT /OUT:"c:\tmp\Test\Debug\Test.exe" /INCREMENTAL /NOLOGO kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /MANIFEST /ManifestFile:"Debug\Test.exe.intermediate.manifest" /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /DEBUG /PDB:"c:\tmp\Test\Debug\Test.pdb" /SUBSYSTEM:CONSOLE /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"c:\tmp\Test\Debug\Test.lib" /MACHINE:X86 Debug\Test.exe.embed.manifest.res Debug\Test.obj
GNU/Linux Equivalent
It is very similar in GNU/Linux. The equivalent of a compiler and linker is gcc
, the GNU project C and C++ compiler. It does the preprocessing, compilation, assembly and linking.
$ gcc --help Usage: gcc [options] file...
Shown below is a very simple Makefile
which can be accessed from GitHub https://github.com/cognitivewaves/Simple-Makefile.
# Specify compiler CC=gcc # Specify linker LINK=gcc .PHONY : all all : app # Link the object files into a binary app : main.o $(LINK) -o app main.o -lstdc++ # Compile the source files into object files main.o : main.cpp $(CC) -c main.cpp -o main.o # Clean target .PHONY : clean clean : rm main.o app
Invoking the make
command to build will output the following.
~/src/Simple-Makefile $ make gcc -c main.cpp -o main.o gcc -o app main.o -lstdc++
Visual Studio to Make Utility mapping
Below is a table relating Visual Studio aspects to Make utility. At a high level, the Project
file is equivalent to a Makefile
.
Visual Studio | make Utility | |
---|---|---|
Command | devenv.exe |
make |
Source structure | Solution (.sln ) has project files (typically in sub-directories) |
Starting at the root, each Makefile can indicate where other Makefile s (typically in sub-directories) exist |
Library build dependency | Solution (.sln ) has projects and build order |
Makefile |
Source files list | Project (.vcproj ) |
Makefile |
Source to Object dependency | Project (.vcproj ) |
Makefile |
Compile and Link options | Project (.vcproj ) |
Makefile |
Compiler | cl.exe |
gcc, g++, c++ (or any other compiler, even cl.exe ) |
Linker | link.exe |
gcc, ld (or any other linker, even link.exe ) |
Example
Download the example sources from GitHub at https://github.com/cognitivewaves/Makefile-Example.
Note that very basic Makefile
constructs are used because the focus is on the concept and not the capabilities of make itself.
Source Structure
Makefile-Example |-- app | |-- Makefile | |-- main.cxx |-- math | |-- advanced | | |-- AdvancedFunctions.cxx | | |-- AdvancedFunctions.h | |-- simple | | |-- SimpleFunctions.cxx | | |-- SimpleFunctions.h | |-- Makefile |-- Makefile
Build
Visual Studio | make Utility |
---|---|
Building in Visual Studio is via a menu item in the IDE or invoking devenv.exe on the .sln file at the command prompt. This will automatically create the necessary directories and build only the files modified after the last build. |
Initiating a build with makefiles is to invoke the make command at the shell prompt. Creating output directories has to be explicitly done either in the Makefile or externally. |
In this example, to keep the makefiles simple, the directories are created at the shell prompt.
~/src/Makefile-Example $ mkdir -p app/bin app/obj math/bin math/obj
The make
utility syntax is shown below. See make
manual pages for details.
make [ -f makefile ] [ options ] ... [ targets ]
Execute the command make
and specify the “root” Makefile
. However, it is more common to change to directory where the “root” Makefile
exists and call make
. This will read the file named Makefile
in the current directory and call the target all
by default.
Notice how make
enters sub-directories to build. This is because of nested Makefiles
which is explained later in the Makefiles Details section.
~/src/Makefile-Example $ makemake -C math make[1]: Entering directory `/home/user/src/Makefile-Example/math' g++ -fPIC -I. -c -o obj/AdvancedFunctions.o advanced/AdvancedFunctions.cxx g++ -fPIC -I. -c -o obj/SimpleFunctions.o simple/SimpleFunctions.cxx g++ -fPIC -shared -o bin/libmath.so obj/AdvancedFunctions.o obj/SimpleFunctions.o make[1]: Leaving directory `/home/user/src/Makefile-Example/math' make -C app make[1]: Entering directory `/home/user/src/Makefile-Example/app' g++ -I../math -c -o obj/main.o main.cxx g++ obj/main.o -L../math/bin -o bin/app.exe -lmath make[1]: Leaving directory `/home/user/src/Makefile-Example/app'
Run
Once the code is built, run the executable. This is nothing specific to makefiles but has been elaborated in case you are not familiar with Linux as you will notice that by default is will fail to run with an error message.
~/src/Makefile-Example $ app/bin/app.exe 4app/bin/app.exe: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
This is because the executable app.exe
requires the shared object libmath.so
which is in a different directory and is not in the system path. Set LD_LIBRARY_PATH
to specify the path to it.
~/src/Makefile-Example $ export LD_LIBRARY_PATH=~/src/Makefile-Example/math/bin/ ~/src/Makefile-Example $ app/bin/app.exe 4The sum of 4 and 4 is 8 The square root of 4 is 2
Makefile Details
The basis of a Makefile
has a very simple structure.
target: dependencies [tab] system commands
The [tab]
separator is very important! Spaces instead of a [tab]
is not the same. You will see rather obscure error messages as shown below.
Makefile:12: *** missing separator. Stop.
Targets
Here target
is a physical file on disk. When the target
is more of a label, then it has to be tagged as .PHONY
to indicate that the target is not an actual file.
Visual Studio | make Utility |
---|---|
Visual Studio by default provides options to clean and rebuild a project or solution. | Clean and rebuild have to be explicitly written in a makefile as targets which can then be invoked. |
A typical case would be to clean
before rebuilding.
~/src/Makefile-Example $ make cleanmake -C math clean make[1]: Entering directory `/home/api/src/Makefile-Example/math' rm -fv bin/* obj/* removed `bin/libmath.so' removed `obj/AdvancedFunctions.o' removed `obj/SimpleFunctions.o' make[1]: Leaving directory `/home/api/src/Makefile-Example/math' make -C app clean make[1]: Entering directory `/home/api/src/Makefile-Example/app' rm -fv bin/* obj/* removed `bin/app.exe' removed `obj/main.o' make[1]: Leaving directory `/home/api/src/Makefile-Example/app'
Dependencies
Dependencies
can be files on disk or other targets (including phony targets).
Visual Studio | make Utility |
---|---|
Visual Studio by default supports implicit dependencies (source to object files) within a project. Library(project) dependencies have to specified in the solution file | Every dependency has to be explicitly defined in makefiles |
For example, the target all
, depends on app.exe
which in turn depends on libmath.so
. If you remove app.exe
, make is capable of recognizing that libmath.so
need not be built again.
~/src/Makefile-Example $ rm -v app/bin/*removed `app/bin/app.exe'~/src/Makefile-Example $ makemake -C app make[1]: Entering directory `/home/api/src/Makefile-Example/app' g++ obj/main.o -L../math/bin -o bin/app.exe -lmath make[1]: Leaving directory `/home/api/src/Makefile-Example/app'
Contents
File: Makefile
# all is the default target .PHONY : all all : app/bin/app.exe # Invoke the Makefile in the sub-directory "app" app/bin/app.exe : math/bin/libmath.so $(MAKE) -C app # Invoke the Makefile in the sub-directory "math" math/bin/libmath.so : $(MAKE) -C math # Invoke the clean target in the "math" and "app" sub-directory Makefile .PHONY : clean clean : $(MAKE) -C math clean $(MAKE) -C app clean
File: math/Makefile
# The shared object libmath.so depends on the object files # The command is to link and create the .so if the dependencies are up to date # Note the .so is put in a sub-directory 'bin' by specifying 'g++ -o' option bin/libmath.so : obj/AdvancedFunctions.o obj/SimpleFunctions.o g++ -fPIC -shared -o bin/libmath.so obj/AdvancedFunctions.o obj/SimpleFunctions.o # The object file depends on the source # The command is to compile the .cxx to .o obj/AdvancedFunctions.o : advanced/AdvancedFunctions.cxx g++ -fPIC -I. -c -o obj/AdvancedFunctions.o advanced/AdvancedFunctions.cxx # The object file depends on the source # The command is to compile the .cxx to .o obj/SimpleFunctions.o : simple/SimpleFunctions.cxx g++ -fPIC -I. -c -o obj/SimpleFunctions.o simple/SimpleFunctions.cxx # clean target for this sub-directory Makefile .PHONY : clean clean : rm -fv bin/* obj/
File: app/Makefile
# The executable depends on the main.o object file # Generate the exe if the object file dependencies are up to date # The exe is put in a sub-directory 'bin' by specifying 'g++ -o' option bin/app.exe : obj/main.o g++ obj/main.o -L../math/bin -o bin/app.exe -lmath # The object file depends on the source # The command is to compile the .cxx to .o # The object file is put in sub-directory 'obj' by specifying 'g++ -o' option obj/main.o : main.cxx g++ -I../math -c -o obj/main.o main.cxx # clean target for this sub-directory Makefile .PHONY : clean clean : rm -fv bin/* obj/
NMake
NMake is the native Windows alternative to the *nix make utility. The syntax is very similar to *nix makefiles. However, this does not mean that *nix makefiles can be executed seamlessly on Windows. See Makefiles in Windows for a discussion.
Conclusion
Makefiles are very powerful and gives a lot of control and flexibility compared to Visual Studio, but the content is not easily understandable. As an alternative, CMake has adopted similar concepts but the script is much easier and more readable. See CMake and Visual Studio.
Pingback: Makefiles for Visual Studio Developers | Cognitive Waves
Thanks for the well written article!
I have 22 years as a C++ on Visual Studio / msbuild, and am one of our in-house experts on .vcxproj / .props / .targets files. Only now I’m having to go back and re-learn makefiles (did some as an undergrad). I think I’m your exact target audience! Thanks for this.
I’m glad it was helpful. I was in a similar situation a few years back when I put this together.