6

I'm looking to save myself some effort further down the line by making a fairly generic makefile that will put together relatively simple C++ projects for me with minimal modifications required to the makefile.

So far I've got it so it will use all .cpp files in the same directory and specified child directories, place all these within a matching structure in a obj subdir and place the resulting file in another subdir called bin. Pretty much what I want.

However, trying to get it so that the required obj and bin directories is created if they don't exist is providing awkward to get working cross-platform - specifically, I'm just testing with Windows 7 & Ubuntu (can't remember version), and I can't get it to work on both at the same time.

Windows misreads mkdir -p dir and creates a -p directory and obviously the two platforms use \ and / respectively for the path separator - and I get errors when using the wrong one.

Here is a few selected portions of the makefile that are relevant:

# Manually edited directories (in this example with forward slashes)
SRC_DIR = src src/subdir1 src/subdir2

# Automagic object directories + the "fixed" bin directory
OBJ_DIR = obj $(addprefix obj/,$(SRC_DIR))
BIN_DIR = bin

# Example build target
debug: checkdirs $(BIN)

# At actual directory creation
checkdirs: $(BIN_DIR) $(OBJ_DIR)
$(BIN_DIR):
    @mkdir $@

$(OBJ_DIR):
    @mkdir -p $@

This has been put together by me over the last week or so from things I've been reading (mostly on Stack Overflow), so if it happens to be I'm following some horrible bad practice or anything of that nature please let me know.

Question in a nutshell:

Is there a simple way to get this directory creation to work from a single makefile in a way that provides as much portability as possible?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
DMA57361
  • 3,600
  • 3
  • 27
  • 36
  • "Those who do not understand auto{make} are doomed to reinvent it, poorly." You can do this with a one-line Makefile.am. Note, I'm actually not recommending you jump to automake, but be aware that these issues have been addressed, often. Eventually, you'll probably move to a tool which has already solved these problems for you, and automake is one such tool. – William Pursell Jul 12 '10 at 12:42
  • @William fair enough. This is as much a learning exercise as anything (prior to this endeavour my understanding of makefiles was very minimal - having built almost everything previously via the makes-it-work-for-you magic of an IDE). By the sounds of things I was aiming for something too deep for a makefile, so maybe I'll add automake to my "things to look at" list for when I've some more time available. – DMA57361 Jul 12 '10 at 13:53

3 Answers3

11

I don't know autoconf. Every experience I've had with it has been tedious. The problem with zwol's solution is that on Windows mkdir returns an error, unlike mkdir -p on Linux. This could break your make rule. The workaround is to ignore the error with - flag before the command, like this:

-mkdir dir

The problem with this is that make still throws an ugly warning for the user. The workaround for this is to run an "always true" command after the mkdir fails as described here, like this:

mkdir dir || true

The problem with this is that Windows and Linux have different syntax for true.

Anyway, I spent too much time on this. I wanted a make file that worked in both POSIX-like and Windows environments. In the end I came up with the following:

ifeq ($(shell echo "check_quotes"),"check_quotes")
   WINDOWS := yes
else
   WINDOWS := no
endif

ifeq ($(WINDOWS),yes)
   mkdir = mkdir $(subst /,\,$(1)) > nul 2>&1 || (exit 0)
   rm = $(wordlist 2,65535,$(foreach FILE,$(subst /,\,$(1)),& del $(FILE) > nul 2>&1)) || (exit 0)
   rmdir = rmdir $(subst /,\,$(1)) > nul 2>&1 || (exit 0)
   echo = echo $(1)
else
   mkdir = mkdir -p $(1)
   rm = rm $(1) > /dev/null 2>&1 || true
   rmdir = rmdir $(1) > /dev/null 2>&1 || true
   echo = echo "$(1)"
endif

The functions/variables are used like so:

rule:
    $(call mkdir,dir)
    $(call echo,  CC      $@)
    $(call rm,file1 file2)
    $(call rmdir,dir1 dir2)

Rationale for the definitions:

  • mkdir: Fix up the path and ignore any errors.
  • del: In Windows del doesn't delete any files if one of the files is specified to be in a directory that doesn't exist. For example, if you try to delete a set of files and dir/file.c is in the list, but dir doesn't exist, no files will be deleted. This implementation works around that issue by invoking del once for each file.
  • rmdir: Fix up the path and ignore any errors.
  • echo: The output's appearance is preserved and doesn't show the extraneous "" in Windows.

I spent a lot of time on this. Perhaps I would have been better off spending my time learning autoconf.

See also:

  1. OS detecting makefile
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Samuel
  • 8,063
  • 8
  • 45
  • 41
  • 1
    This is exactly what I was looking for. However for some reason MinGW crashed with a "Interrupt/Exception caught (code = 0xc0000005)". After debugging for a while, I found that it has something to do with ending characters, so I put it inside of a $(shell) and everything worked fine. Also, there's a simpler solution for mkdir on Windows using "if not exist": mkdir = $(shell if not exist $(subst /,\,$(1)) mkdir $(subst /,\,$(1))) – Hugo Aboud Aug 16 '20 at 23:19
9

Windows mkdir always does what Unix mkdir does with the -p switch on. And you can deal with the backslash problem with $(subst). So, on Windows, you want this:

$(BIN_DIR) $(OBJ_DIR):
        mkdir $(subst /,\\,$@)

and on Unix you want this:

$(BIN_DIR) $(OBJ_DIR):
        mkdir -p -- $@

Choosing between these is not practical to do within a makefile. This is what Autoconf is for.

As a side note, never, ever use the @command feature in your makefiles. There will come a day when you need to debug your build process on a machine you do not have direct access to, and on that day, you will regret it.

zwol
  • 135,547
  • 38
  • 252
  • 361
  • OK, that's handy, minimising the required differences between versions is what I'm aiming for until I move on to learning any sort of auto based version. But, would you please mind briefly explaining what the `@command` feature *does*? As said in my question, I have built the makefile from various sources with minimal previous knowledge, so must have pulled it from *somewhere*, but now can't remember from where or the relevance... – DMA57361 Jul 16 '10 at 07:39
  • 2
    If you stick an `@` in front of any Makefile command, Make doesn't print the command to the terminal before it executes it. This *seems* like a great feature -- less noise scrolling by, yay -- until you have to debug your build process using only that terminal transcript, and then you are in a world of hurt. – zwol Jul 16 '10 at 17:42
  • About the last statement: this behavior (echoing or not echoing a command) can be easily driven through make command-line, by defining (or not) a variable holding the `@`, and writing each command with this variable in front. See http://skramm.blogspot.fr/2013/04/writing-portable-makefiles.html, section 5 – kebs Oct 11 '13 at 15:43
0

I solved the portability problem by creating a Python script called mkdir.py and calling it from the Makefile. A limitation is that Python must be installed, but this is most likely true for any version of UNIX.

#!/usr/bin/env python

# Cross-platform mkdir command.

import os
import sys

if __name__=='__main__':
    if len(sys.argv) != 2:
        sys.exit('usage: mkdir.py <directory>')
    directory = sys.argv[1]
    try:
        os.makedirs(directory)
    except OSError:
        pass
Nathan Farrington
  • 1,890
  • 1
  • 17
  • 27