Jeff Nyman
Jeff Nyman

Reputation: 890

Evolving a Makefile From Flat Directory Structure to Sub-Directory Structure

SEE UPDATES BELOW


Research Done: I'm finding learning how to evolve Makefiles from one situation to another is difficult. There are a ton of questions and answers out there but few of them actually show how a Makefile can evolve as your project changes. They also all seem to use various different techniques and idioms of Makefiles so translating between one question and another can be tricky when you are learning Makefiles for the first time, as I am.

Problem: My problem is that I have a project that started at as a flat directory structure but then is migrating to a structure with sub-directories. What I can't do is get my Makefile to along for the ride.

First I'll show what I created that works and then I show how I want it to evolve and how that doesn't work.

Flat Directory Structure, Working Makefile

I have project directory that has all my C files and one header file plus my Makefile:

project
  Makefile
  c8_asm.c
  c8_dasm.c
  c8_terp.c
  chip8.h

Here is my Makefile (which works just fine):

CC = gcc

CFLAGS += -c -Wall -std=c99
CFLAGS += -D_POSIX_C_SOURCE=200809L

LDLIBS += -lm

# Targets

all: c8_dasm c8_asm c8_terp

c8_dasm: c8_dasm.o
    $(CC) $(LDLIBS) c8_dasm.o -o $@

c8_asm: c8_asm.o
    $(CC) $(LDLIBS) c8_asm.o -o $@

c8_terp: c8_terp.o
    $(CC) $(LDLIBS) c8_terp.o -o $@

# Using implicit rules for updating an '.o' file from a correspondingly
# named '.c' file.

c8_dasm.o: chip8.h
c8_asm.o: chip8.h
c8_terp.o: chip8.h

.PHONY: clean
clean:
    rm c8_dasm c8_asm c8_terp c8_dasm.o c8_asm.o c8_terp.o

I get all my .o files and my executables are created in the project directory.

Evolving The Project

But what I wanted to do is have my sources files (all .c and .h) in a src directory. I wanted to build into an obj directory and have the executables go in a bin directory. So my project would look like this:

project
  src
    c8_asm.c
    c8_dasm.c
    c8_terp.c
    chip8.h
  Makefile

Sub-Directory Structure, Makefile NOT Working

To accommodate the above, I changed my Makefile accordingly:

CC = gcc

CFLAGS += -c -Wall -std=c99
CFLAGS += -D_POSIX_C_SOURCE=200809L

LDLIBS += -lm

SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin

SOURCES := $(wildcard $(SRC_DIR)/*.c)
OBJECTS := $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)

MKDIR_P ?= mkdir -p

# Targets

all: $(BIN_DIR)/c8_dasm $(BIN_DIR)/c8_asm $(BIN_DIR)/c8_terp

$(BIN_DIR)/c8_dasm: $(OBJ_DIR)/c8_dasm.o
    $(CC) $(LDLIBS) $(OBJ_DIR)/c8_dasm.o -o $@

$(BIN_DIR)/c8_asm: $(OBJ_DIR)/c8_asm.o
    $(CC) $(LDLIBS) $(OBJ_DIR)/c8_asm.o -o $@

$(BIN_DIR)/c8_terp: $(OBJ_DIR)/c8_terp.o
    $(MKDIR_P) $(dir $@)
    $(CC) $(LDLIBS) $(OBJ_DIR)/c8_terp.o -o $@

$(OBJECTS): $(OBJ_DIR)/%.o : $(SRC_DIR)/%.c
    $(MKDIR_P) $(dir $@)
    $(CC) $< -o $(OBJ_DIR)/$@

# Using implicit rules for updating an '.o' file from a correspondingly
# named '.c' file.

$(OBJ_DIR)/c8_dasm.o: $(SRC_DIR)/chip8.h
$(OBJ_DIR)/c8_asm.o: $(SRC_DIR)/chip8.h
$(OBJ_DIR)/c8_terp.o: $(SRC_DIR)/chip8.h

.PHONY: clean
clean:
    rm -r $(BUILD_DIR)
    rm $(OBJECTS)

Upon running this I get the following:

mkdir -p obj/obj/
gcc src/c8_dasm.c -o obj/c8_dasm.o
gcc -lm obj/c8_dasm.o -o bin/c8_dasm
ld: can't link with a main executable file 'obj/c8_dasm.o' for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [bin/c8_dasm] Error 1

I wanted to stop here and get some assistance because I fear I'm making this Makefile for complicated than it need be and I'm trying to avoid getting into bad habits.

I'm hoping to hear opinions about what I'm not conceptualizing correctly here.

FIRST UPDATE

I managed to take it bit by bit and get it mostly working. Here is what I ended up with:

CC = gcc

CFLAGS += -c -Wall -std=c99
CFLAGS += -D_POSIX_C_SOURCE=200809L

LDLIBS += -lm

# Directories.

SRC_DIR = src
BIN_DIR = bin

$(shell mkdir -p $(BIN_DIR))

# Patterns for files.

SOURCES := $(wildcard $(SRC_DIR)/*.c)
OBJECTS := $(SOURCES:$(SRC_DIR)/%.c=$(SRC_DIR)/%.o)

EXECUTABLES := c8_dasm c8_asm c8_terp

# Targets

all: $(EXECUTABLES)

c8_dasm: $(SRC_DIR)/c8_dasm.o
    $(CC) $^ $(LDLIBS) -o $(BIN_DIR)/$@
    @echo "C8 Disassembler Built"

c8_asm: $(SRC_DIR)/c8_asm.o
    $(CC) $^ $(LDLIBS) -o $(BIN_DIR)/$@
    @echo "C8 Assembler Built"

c8_terp: $(SRC_DIR)/c8_terp.o
    $(CC) $^ $(LDLIBS) -o $(BIN_DIR)/$@
    @echo "C8 Interpreter Built"

# Using implicit rules for updating an '.o' file from a correspondingly
# named '.c' file.

c8_dasm.o: $(SRC_DIR)/chip8.h
c8_asm.o: $(SRC_DIR)/chip8.h
c8_terp.o: $(SRC_DIR)/chip8.h

.PHONY: clean
clean:
    rm $(OBJECTS)
    rm -r $(BIN_DIR)

Of course, as I'm finding with Make this leads to other obscure problems. For example doing this:

make
make clean

works fine. Meaning all files are generated and the files are cleaned, including the bin directory.

However, if I do this:

make c8_dasm
make clean

This builds fine. But the clean fails to delete the bin directory (although it does delete the object files). This happens regardless of what individual executable I try to build.

No amount of searching is helping me find out why that is.

SECOND UPDATE

I found that problem was solved as well. It just required using the "-f" for the rm statements in the clean target.

THIRD UPDATE

To get the object file directory part working, I tried (from this: path include and src directory makefile) to construct my Makefile as follows:

CC = gcc

CFLAGS += -c -Wall -std=c99
CFLAGS += -D_POSIX_C_SOURCE=200809L

LDLIBS += -lm

SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin

$(shell mkdir -p $(BIN_DIR))
$(shell mkdir -p $(OBJ_DIR))

SOURCES := $(wildcard $(SRC_DIR)/*.c)
OBJECTS := $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)

EXECUTABLES := c8_dasm c8_asm c8_terp

all: $(EXECUTABLES)

c8_dasm: $(SRC_DIR)/c8_dasm.o
    $(CC) $^ $(LDLIBS) -o $(BIN_DIR)/$@
    @echo "C8 Disassembler Built"

c8_asm: $(SRC_DIR)/c8_asm.o
    $(CC) $^ $(LDLIBS) -o $(BIN_DIR)/$@
    @echo "C8 Assembler Built"

c8_terp: $(SRC_DIR)/c8_terp.o
    $(CC) $^ $(LDLIBS) -o $(BIN_DIR)/$@
    @echo "C8 Interpreter Built"

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
    $(CC) $(CFLAGS) -c $< -o $(BIN_DIR)/$@

.PHONY: clean
clean:
    rm -rf $(BIN_DIR)
    rm -f $(OBJECTS)

I was able to condense the original three lines using chip8.h into one target but I have no way to know if that's correct. It compiles at least. I also changed the OBJECTS line to reflect the new OBJ_DIR I created.

However, this doesn't put the object files in the right place. It still puts them in the src directory rather than the obj directory.

Upvotes: 3

Views: 173

Answers (3)

Jeff Nyman
Jeff Nyman

Reputation: 890

I believe I may have figured this out. Below is my Makefile. It seems to do what I want. It does the following:

  • Compiles all object files into the obj directory.
  • Compiles and links so that executables are generated in the bin directory.
  • Recognizes if any .c files are changed and recompiles accordingly.
  • Recognizes if the .h file is changed and recompiles all C files that reference it.

This seems to satisfy all the criteria but I can't tell if I've painted myself into some corner that I can't see yet.

CC = gcc

CFLAGS += -c -Wall -std=c99
CFLAGS += -D_POSIX_C_SOURCE=200809L

LDLIBS += -lm

SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin

$(shell mkdir -p $(BIN_DIR))
$(shell mkdir -p $(OBJ_DIR))

SOURCES := $(wildcard $(SRC_DIR)/*.c)
OBJECTS := $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)

EXECUTABLES := c8_dasm c8_asm c8_terp

all: $(EXECUTABLES)

c8_dasm: $(OBJ_DIR)/c8_dasm.o
    $(CC) $^ $(LDLIBS) -o $(BIN_DIR)/$@
    @echo "C8 Disassembler Built"

c8_asm: $(OBJ_DIR)/c8_asm.o
    $(CC) $^ $(LDLIBS) -o $(BIN_DIR)/$@
    @echo "C8 Assembler Built"

c8_terp: $(OBJ_DIR)/c8_terp.o
   $(CC) $^ $(LDLIBS) -o $(BIN_DIR)/$@
   @echo "C8 Interpreter Built"

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c $(SRC_DIR)/chip8.h
    $(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:
    rm -rf $(BIN_DIR)
    rm -rf $(OBJ_DIR)

Upvotes: 1

Micah Condritch
Micah Condritch

Reputation: 59

Stackoverflow is whining about too many comments, so I'll make this another "answer." After our back-and-forth to my original comment, your last comment is correct. That's what I wanted you to see.

Understand that you can't use Make to do what you want to do exactly.

So here's really the answer: You can't create multiple executables AND with only some of the object files applying to each one AND while using a directory structure. Make is in no way capable of handling that.

Right now you're trying to use Make in a way that it wasn't intended for which is why you're running into so many problems. If you keep playing around you're going to run into is a series of errors that say "duplicate symbol" because you will be compiling each of your files multiple times for each executable, assuming you follow most of the advice you'll find.

Check out this How can I create a Makefile for C projects with SRC, OBJ, and BIN subdirectories? to see what I mean. That one works because all object files are being used to create a single executable. But as you've stated, that's not going to be the case for you. And that's what Make can't handle. That's why you're not finding an answer to that.

And while your chip8.h file is now not going to cause problems in terms of allowing you to compile, your Makefile with that third update would not recognize when the chip8.h file itself has changed. You would have to change a .c file to force a recompile so that changes to your .h were recognized. So you either have to stick with your second update or use something other than Make.

Upvotes: 0

Micah Condritch
Micah Condritch

Reputation: 59

This is why it makes sense to not do anything complicated with Makefiles. Just put the actual directory names in your commands. Never rely on wildcards.

People using C and C++ and using Makefiles spend too much time trying to get those to work rather than just actually getting things done. That's why you see so many of the questions that you see and why the answers vary so much.

In your specific case, your targets don't always have to contain the directory and that's part of the problem. The rules getting generated don't have an actual target in your file because of the directories you are prepending to everything. You have to think in terms of what is getting generated by each target: meaning, the output. So if c8_dasm is getting output, that's your target. The directory has nothing to do with that. So you need to remove all of your directory substitutions where they aren't needed.

But before doing that, ask yourself this: if your first solution was working, why change it? It's better to not even do directories when you're using Make. Just have everything in the same directory as you started off with. You can even see that this allows your Makefile to be much cleaner.

Upvotes: 1

Related Questions