I routinely work on several different computers and several different operating systems, which are Mac OS X, Linux, or Solaris. For the project I'm working on, I pull my code from a remote git repository.
I like to be able to work on my projects regardless of which terminal I'm at. So far, I've found ways to get around the OS changes by changing the makefile every time I switch computers. However, this is tedious and causes a bunch of headaches.
How can I modify my makefile so that it detects which OS I'm using and modifies syntax accordingly?
Here is the makefile:
cc = gcc -g
CC = g++ -g
yacc=$(YACC)
lex=$(FLEX)
all: assembler
assembler: y.tab.o lex.yy.o
$(CC) -o assembler y.tab.o lex.yy.o -ll -l y
assembler.o: assembler.c
$(cc) -o assembler.o assembler.c
y.tab.o: assem.y
$(yacc) -d assem.y
$(CC) -c y.tab.c
lex.yy.o: assem.l
$(lex) assem.l
$(cc) -c lex.yy.c
clean:
rm -f lex.yy.c y.tab.c y.tab.h assembler *.o *.tmp *.debug *.acts
There are many good answers here already, but I wanted to share a more complete example that both:
doesn't assume uname exists on Windows
also detects the processor
The CCFLAGS defined here aren't necessarily recommended or ideal; they're just what the project to which I was adding OS/CPU auto-detection happened to be using.
ifeq ($(OS),Windows_NT)
CCFLAGS += -D WIN32
ifeq ($(PROCESSOR_ARCHITEW6432),AMD64)
CCFLAGS += -D AMD64
else
ifeq ($(PROCESSOR_ARCHITECTURE),AMD64)
CCFLAGS += -D AMD64
endif
ifeq ($(PROCESSOR_ARCHITECTURE),x86)
CCFLAGS += -D IA32
endif
endif
else
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
CCFLAGS += -D LINUX
endif
ifeq ($(UNAME_S),Darwin)
CCFLAGS += -D OSX
endif
UNAME_P := $(shell uname -p)
ifeq ($(UNAME_P),x86_64)
CCFLAGS += -D AMD64
endif
ifneq ($(filter %86,$(UNAME_P)),)
CCFLAGS += -D IA32
endif
ifneq ($(filter arm%,$(UNAME_P)),)
CCFLAGS += -D ARM
endif
endif
The uname command (http://developer.apple.com/documentation/Darwin/Reference/ManPages/man1/uname.1.html) with no parameters should tell you the operating system name. I'd use that, then make conditionals based on the return value.
Example
UNAME := $(shell uname)
ifeq ($(UNAME), Linux)
# do something Linux-y
endif
ifeq ($(UNAME), Solaris)
# do something Solaris-y
endif
Detect the operating system using two simple tricks:
First the environment variable OS
Then the uname command
ifeq ($(OS),Windows_NT) # is Windows_NT on XP, 2000, 7, Vista, 10...
detected_OS := Windows
else
detected_OS := $(shell uname) # same as "uname -s"
endif
Or a more safe way, if not on Windows and uname
unavailable:
ifeq ($(OS),Windows_NT)
detected_OS := Windows
else
detected_OS := $(shell sh -c 'uname 2>/dev/null || echo Unknown')
endif
Ken Jackson proposes an interesting alternative if you want to distinguish Cygwin/MinGW/MSYS/Windows. See his answer that looks like that:
ifeq '$(findstring ;,$(PATH))' ';'
detected_OS := Windows
else
detected_OS := $(shell uname 2>/dev/null || echo Unknown)
detected_OS := $(patsubst CYGWIN%,Cygwin,$(detected_OS))
detected_OS := $(patsubst MSYS%,MSYS,$(detected_OS))
detected_OS := $(patsubst MINGW%,MSYS,$(detected_OS))
endif
Then you can select the relevant stuff depending on detected_OS
:
ifeq ($(detected_OS),Windows)
CFLAGS += -D WIN32
endif
ifeq ($(detected_OS),Darwin) # Mac OS X
CFLAGS += -D OSX
endif
ifeq ($(detected_OS),Linux)
CFLAGS += -D LINUX
endif
ifeq ($(detected_OS),GNU) # Debian GNU Hurd
CFLAGS += -D GNU_HURD
endif
ifeq ($(detected_OS),GNU/kFreeBSD) # Debian kFreeBSD
CFLAGS += -D GNU_kFreeBSD
endif
ifeq ($(detected_OS),FreeBSD)
CFLAGS += -D FreeBSD
endif
ifeq ($(detected_OS),NetBSD)
CFLAGS += -D NetBSD
endif
ifeq ($(detected_OS),DragonFly)
CFLAGS += -D DragonFly
endif
ifeq ($(detected_OS),Haiku)
CFLAGS += -D Haiku
endif
Notes:
Command uname is same as uname -s because option -s (--kernel-name) is the default. See why uname -s is better than uname -o.
The use of OS (instead of uname) simplifies the identification algorithm. You can still use solely uname, but you have to deal with if/else blocks to check all MinGW, Cygwin, etc. variations.
The environment variable OS is always set to "Windows_NT" on different Windows versions (see %OS% environment variable on Wikipedia).
An alternative of OS is the environment variable MSVC (it checks the presence of MS Visual Studio, see example using Visual C++).
Below I provide a complete example using make
and gcc
to build a shared library: *.so
or *.dll
depending on the platform. The example is as simplest as possible to be more understandable.
To install make
and gcc
on Windows see Cygwin or MinGW.
My example is based on five files
├── lib
│ └── Makefile
│ └── hello.h
│ └── hello.c
└── app
└── Makefile
└── main.c
Reminder: Makefile
is indented using tabulation. Caution when copy-pasting below sample files.
The two Makefile files
1. lib/Makefile
ifeq ($(OS),Windows_NT)
uname_S := Windows
else
uname_S := $(shell uname -s)
endif
ifeq ($(uname_S), Windows)
target = hello.dll
endif
ifeq ($(uname_S), Linux)
target = libhello.so
endif
#ifeq ($(uname_S), .....) #See https://stackoverflow.com/a/27776822/938111
# target = .....
#endif
%.o: %.c
gcc -c $< -fPIC -o $@
# -c $< => $< is first file after ':' => Compile hello.c
# -fPIC => Position-Independent Code (required for shared lib)
# -o $@ => $@ is the target => Output file (-o) is hello.o
$(target): hello.o
gcc $^ -shared -o $@
# $^ => $^ expand to all prerequisites (after ':') => hello.o
# -shared => Generate shared library
# -o $@ => Output file (-o) is $@ (libhello.so or hello.dll)
2. app/Makefile
ifeq ($(OS),Windows_NT)
uname_S := Windows
else
uname_S := $(shell uname -s)
endif
ifeq ($(uname_S), Windows)
target = app.exe
endif
ifeq ($(uname_S), Linux)
target = app
endif
#ifeq ($(uname_S), .....) #See https://stackoverflow.com/a/27776822/938111
# target = .....
#endif
%.o: %.c
gcc -c $< -I ../lib -o $@
# -c $< => compile (-c) $< (first file after :) = main.c
# -I ../lib => search headers (*.h) in directory ../lib
# -o $@ => output file (-o) is $@ (target) = main.o
$(target): main.o
gcc $^ -L../lib -lhello -o $@
# $^ => $^ (all files after the :) = main.o (here only one file)
# -L../lib => look for libraries in directory ../lib
# -lhello => use shared library hello (libhello.so or hello.dll)
# -o $@ => output file (-o) is $@ (target) = "app.exe" or "app"
To learn more, read Automatic Variables documentation as pointed out by cfi.
The source code
- lib/hello.h
#ifndef HELLO_H_
#define HELLO_H_
const char* hello();
#endif
- lib/hello.c
#include "hello.h"
const char* hello()
{
return "hello";
}
- app/main.c
#include "hello.h" //hello()
#include <stdio.h> //puts()
int main()
{
const char* str = hello();
puts(str);
}
The build
Fix the copy-paste of Makefile
(replace leading spaces by one tabulation).
> sed 's/^ */\t/' -i */Makefile
The make
command is the same on both platforms. The given output is on Unix-like OSes:
> make -C lib
make: Entering directory '/tmp/lib'
gcc -c hello.c -fPIC -o hello.o
# -c hello.c => hello.c is first file after ':' => Compile hello.c
# -fPIC => Position-Independent Code (required for shared lib)
# -o hello.o => hello.o is the target => Output file (-o) is hello.o
gcc hello.o -shared -o libhello.so
# hello.o => hello.o is the first after ':' => Link hello.o
# -shared => Generate shared library
# -o libhello.so => Output file (-o) is libhello.so (libhello.so or hello.dll)
make: Leaving directory '/tmp/lib'
> make -C app
make: Entering directory '/tmp/app'
gcc -c main.c -I ../lib -o main.o
# -c main.c => compile (-c) main.c (first file after :) = main.cpp
# -I ../lib => search headers (*.h) in directory ../lib
# -o main.o => output file (-o) is main.o (target) = main.o
gcc main.o -L../lib -lhello -o app
# main.o => main.o (all files after the :) = main.o (here only one file)
# -L../lib => look for libraries in directory ../lib
# -lhello => use shared library hello (libhello.so or hello.dll)
# -o app => output file (-o) is app.exe (target) = "app.exe" or "app"
make: Leaving directory '/tmp/app'
The run
The application requires to know where is the shared library.
On Windows, a simple solution is to copy the library where the application is:
> cp -v lib/hello.dll app
`lib/hello.dll' -> `app/hello.dll'
On Unix-like OSes, you can use the LD_LIBRARY_PATH
environment variable:
> export LD_LIBRARY_PATH=lib
Run the command on Windows:
> app/app.exe
hello
Run the command on Unix-like OSes:
> app/app
hello
uname
is not Linux. I give just an example you may not need, but this may help someone searching (on the web) a way to implement a Makefile
for both platforms ;-) What should I change in my answer? Cheers
lib/Makefile
example, target
is used for .so
vs .dll
. A parallel example for the app/makefile
would be useful for the empty string
vs .exe
comparison for app filename. For instance, I don't usually see app.exe
on Unix-like OSes. ;-)
I was recently experimenting in order to answer this question I was asking myself. Here are my conclusions:
Since in Windows, you can't be sure that the uname
command is available, you can use gcc -dumpmachine
. This will display the compiler target.
There may be also a problem when using uname
if you want to do some cross-compilation.
Here's a example list of possible output of gcc -dumpmachine
:
mingw32
i686-pc-cygwin
x86_64-redhat-linux
You can check the result in the makefile like this:
SYS := $(shell gcc -dumpmachine)
ifneq (, $(findstring linux, $(SYS)))
# Do Linux things
else ifneq(, $(findstring mingw, $(SYS)))
# Do MinGW things
else ifneq(, $(findstring cygwin, $(SYS)))
# Do Cygwin things
else
# Do things for others
endif
It worked well for me, but I'm not sure it's a reliable way of getting the system type. At least it's reliable about MinGW and that's all I need since it does not require to have the uname
command or MSYS package in Windows.
To sum up, uname
gives you the system on which you're compiling, and gcc -dumpmachine
gives you the system for which you are compiling.
uname
come with MinGW
anyway? Nevertheless, the extra note regarding cross-compilation is great.
$(shell $(CC) -dumpmachine)
. As of OS X Sierra, the -dumpmachine command works on Clang.
x86_64-apple-darwin16.6.0
and that works wether you call it gcc
, cc
, or clang
, but not cl
The git makefile contains numerous examples of how to manage without autoconf/automake, yet still work on a multitude of unixy platforms.
if (!usesAutotools(git)) aversionTo(autotools) = justified;
I'll clarify also that it's only the tools I'm averse to. I'm sure the Autotools people are nice folks.
Update: I now consider this answer to be obsolete. I posted a new perfect solution further down.
If your makefile may be running on non-Cygwin Windows, uname
may not be available. That's awkward, but this is a potential solution. You have to check for Cygwin first to rule it out, because it has WINDOWS in its PATH
environment variable too.
ifneq (,$(findstring /cygdrive/,$(PATH)))
UNAME := Cygwin
else
ifneq (,$(findstring WINDOWS,$(PATH)))
UNAME := Windows
else
UNAME := $(shell uname -s)
endif
endif
uname
from a normal cmd terminal it gives me MINGW. What I mean is, I still have uname
without using Cygwin. I also have git bash, but I haven't tried uname on it (right now I'm in Linux). Can you tell me how these two can be incorporated in your code?
uname
couldn't be run we would understand we are in Windows?
That's the job that GNU's automake/autoconf are designed to solve. You might want to investigate them.
Alternatively you can set environment variables on your different platforms and make you Makefile conditional against them.
make
to do what I want. Do I now want to get into automake/autoconf as well? - NO. What can be done in the makefile, certainly should be done in the makefile, if only so that I don't have several stop-off points every time I want to amend compile & link.
I ran into this problem today and I needed it on Solaris so here is a POSIX standard way to do (something very close to) this.
#Detect OS
UNAME = `uname`
# Build based on OS name
DetectOS:
-@make $(UNAME)
# OS is Linux, use GCC
Linux: program.c
@SHELL_VARIABLE="-D_LINUX_STUFF_HERE_"
rm -f program
gcc $(SHELL_VARIABLE) -o program program.c
# OS is Solaris, use c99
SunOS: program.c
@SHELL_VARIABLE="-D_SOLARIS_STUFF_HERE_"
rm -f program
c99 $(SHELL_VARIABLE) -o program program.c
I finally found the perfect solution that solves this problem for me.
ifeq '$(findstring ;,$(PATH))' ';'
UNAME := Windows
else
UNAME := $(shell uname 2>/dev/null || echo Unknown)
UNAME := $(patsubst CYGWIN%,Cygwin,$(UNAME))
UNAME := $(patsubst MSYS%,MSYS,$(UNAME))
UNAME := $(patsubst MINGW%,MSYS,$(UNAME))
endif
The UNAME variable is set to Linux, Cygwin, MSYS, Windows, FreeBSD, NetBSD (or presumably Solaris, Darwin, OpenBSD, AIX, HP-UX), or Unknown. It can then be compared throughout the remainder of the Makefile to separate any OS-sensitive variables and commands.
The key is that Windows uses semicolons to separate paths in the PATH variable whereas everyone else uses colons. (It's possible to make a Linux directory with a ';' in the name and add it to PATH, which would break this, but who would do such a thing?) This seems to be the least risky method to detect native Windows because it doesn't need a shell call. The Cygwin and MSYS PATH use colons so uname is called for them.
Note that the OS environment variable can be used to detect Windows, but not to distinguish between Cygwin and native Windows. Testing for the echoing of quotes works, but it requires a shell call.
Unfortunately, Cygwin adds some version information to the output of uname, so I added the 'patsubst' calls to change it to just 'Cygwin'. Also, uname for MSYS actually has three possible outputs starting with MSYS or MINGW, but I use also patsubst to transform all to just 'MSYS'.
If it's important to distinguish between native Windows systems with and without some uname.exe on the path, this line can be used instead of the simple assignment:
UNAME := $(shell uname 2>NUL || echo Windows)
Of course in all cases GNU make is required, or another make which supports the functions used.
Here's a simple solution that checks if you are in a Windows or posix-like (Linux/Unix/Cygwin/Mac) environment:
ifeq ($(shell echo "check_quotes"),"check_quotes")
WINDOWS := yes
else
WINDOWS := no
endif
It takes advantage of the fact that echo exists on both posix-like and Windows environments, and that in Windows the shell does not filter the quotes.
$PATH
might refer to another echo
(Mine does...)
-mwindows
flag or chose between a .dll
or .so
, this would fail.
Note that Makefiles are extremely sensitive to spacing. Here's an example of a Makefile that runs an extra command on OS X and which works on OS X and Linux. Overall, though, autoconf/automake is the way to go for anything at all non-trivial.
UNAME := $(shell uname -s) CPP = g++ CPPFLAGS = -pthread -ansi -Wall -Werror -pedantic -O0 -g3 -I /nexopia/include LDFLAGS = -pthread -L/nexopia/lib -lboost_system HEADERS = data_structures.h http_client.h load.h lock.h search.h server.h thread.h utility.h OBJECTS = http_client.o load.o lock.o search.o server.o thread.o utility.o vor.o all: vor clean: rm -f $(OBJECTS) vor vor: $(OBJECTS) $(CPP) $(LDFLAGS) -o vor $(OBJECTS) ifeq ($(UNAME),Darwin) # Set the Boost library location install_name_tool -change libboost_system.dylib /nexopia/lib/libboost_system.dylib vor endif %.o: %.cpp $(HEADERS) Makefile $(CPP) $(CPPFLAGS) -c $
Another way to do this is by using a "configure" script. If you are already using one with your makefile, you can use a combination of uname and sed to get things to work out. First, in your script, do:
UNAME=uname
Then, in order to put this in your Makefile, start out with Makefile.in which should have something like
UNAME=@@UNAME@@
in it.
Use the following sed command in your configure script after the UNAME=uname
bit.
sed -e "s|@@UNAME@@|$UNAME|" < Makefile.in > Makefile
Now your makefile should have UNAME
defined as desired. If/elif/else statements are all that's left!
I had a case where I had to detect the difference between two versions of Fedora, to tweak the command-line options for inkscape:
- in Fedora 31, the default inkscape is 1.0beta which uses --export-file
- in Fedora < 31, the default inkscape is 0.92 which uses --export-pdf
My Makefile contains the following
# set VERSION_ID from /etc/os-release
$(eval $(shell grep VERSION_ID /etc/os-release))
# select the inkscape export syntax
ifeq ($(VERSION_ID),31)
EXPORT = export-file
else
EXPORT = export-pdf
endif
# rule to convert inkscape SVG (drawing) to PDF
%.pdf : %.svg
inkscape --export-area-drawing $< --$(EXPORT)=$@
This works because /etc/os-release
contains a line
VERSION_ID=<value>
so the shell command in the Makefile returns the string VERSION_ID=<value>
, then the eval command acts on this to set the Makefile variable VERSION_ID
. This can obviously be tweaked for other OS's depending how the metadata is stored. Note that in Fedora there is not a default environment variable that gives the OS version, otherwise I would have used that!
An alternate way that I have not seen anyone talking about is using the built-in variable SHELL
. The program used as the shell is taken from the variable SHELL
. On MS-Windows systems, it is most likely to be an executable file with .exe
extension (like sh.exe
).
In that case, the following conditional test:
ifeq ($(suffix $(SHELL)),.exe)
# Windows system
else
# Non-Windows system
endif
Would have the same result as using the environment variable OS
:
ifeq ($(OS),Windows_NT)
# Windows system
else
# Non-Windows system
endif
However, it seems the latter is the most popular solution, so I would recommend you stick with it.
Success story sharing
PROCESSOR_ARCHITECTURE
envvar seems to be virtualized depending on whether the process is 32-bit or 64-bit. So if yourmake
is 32-bit and you're trying to build a 64-bit application, it will fail. Using it in combination withPROCESSOR_ARCHITEW6432
worked for me (see this, and that)make
team add a couple of magic variables with os and arch, probably too much trouble.OS
is set on non-Windows systems. Make treats unset the same as empty, which will cause a jump to theuname
-based block. You just need to add a FreeBSD check there./bin/sh: -c: line 0: syntax error near unexpected token
,Windows_NT' /bin/sh: -c: line 0:ifeq (,Windows_NT)' make: *** [os] Error 2
if
,else
andendif
MUST NOT be indented (in my experiments). I also only got this to work outside of the target blocks