Makefile: множественное определение и ошибка неопределенной ссылки

В настоящее время я изучаю, как кодировать без IDE, и поэтому я учусь писать make-файлы. Вот мой текущий тестовый проект:

\__ /CoDstructor/
      |\__ Makefile
      |\__ /bin/
      |      \__ CoDstructor.exe
      |\__ /src/
      |     \__ /cod/
      |          |\__ main.cpp
      |          |\__ types.cpp
      |           \__ types.hpp
       \__ /obj/
            \__ /cod/
                 |\__ main.o
                 |\__ main.d
                 |\__ types.o
                  \__ types.d

У меня есть только один make-файл верхнего уровня, который обрабатывает каждый модуль в каталоге src/ и создает объекты и файлы зависимостей в каталоге obj/. Вот файлы:

main.cpp

#include <iostream>
#include <cod/types.hpp>

int main() {
    int s;
    std::cin >> s;
    return lol();
}

типы.hpp

#ifndef TYPES_HPP_INCLUDED
#define TYPES_HPP_INCLUDED

int lol();

#endif // TYPES_HPP_INCLUDED

типы.cpp

#include <iostream>
#include <cod/types.hpp>

int lol() {
    std::cout << "lol";

    return 0;
}

Makefile

APP_NAME = CoDstructor
DEBUG_TARGET = debug
RELEASE_TARGET = release
SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin
INC_DIR = src

INCLUDE_DIRS +=

LIBRARY_DIRS +=

CXXFLAGS += -Wall
CXXFLAGS += -Werror
CXXFLAGS += -Wextra
CXXFLAGS += -pedantic
CXXFLAGS += -std=c++11

$(DEBUG_TARGET): CXXFLAGS += -g
$(RELEASE_TARGET): CXXFLAGS += -O3

LDFLAGS += -static
LDFLAGS += -static-libstdc++
LDFLAGS += -static-libgcc

$(DEBUG_TARGET): LDFLAGS += -g
$(RELEASE_TARGET): LDFLAGS +=

CPPMACROS =

$(DEBUG_TARGET): CPPMACROS += DEBUG
$(RELEASE_TARGET): CPPMACROS += NDEBUG

CXXFLAGS += $(foreach i,$(INC_DIR),$(addprefix -I,$(i)))
CXXFLAGS += $(foreach i,$(INCLUDE_DIRS),$(addprefix -I,$(i)))
CXXFLAGS += $(foreach i,$(CPPMACROS),$(addprefix -D,$(i)))

LIBS = $(foreach i,$(LIBRARY_DIRS),$(addprefix -L,$(i)))

SOURCES =  $(subst ./,,$(shell find . -name *.cpp))
OBJS = $(subst $(SRC_DIR),$(OBJ_DIR),$(SOURCES:.cpp=.o))

DEPS = $(OBJS:.o=.d)

all: $(DEBUG_TARGET) clean

$(RELEASE_TARGET): $(BIN_DIR)/$(APP_NAME) clean
    @echo Building release...

$(DEBUG_TARGET): $(BIN_DIR)/$(APP_NAME) clean
    @echo Building debug...

$(OBJS): $(SOURCES)
    @mkdir -p $(@D)
    $(CXX) -MMD -MP $(CXXFLAGS) -c $< -o $@

$(BIN_DIR)/$(APP_NAME): $(OBJS)
    $(CXX) $(LDFLAGS) $^ -o $@ $(LIBS)
    @echo $^

.PHONY: clean

clean:
    @echo clean
#   @-rmdir $(OBJ_DIR)

-include $(DEPS)

И вот ошибка:

obj/cod/types.o: In function `main':
D:\PROJECTS\CoDstructor/src/cod/main.cpp:4: multiple definition of `main'
obj/cod/main.o:D:\PROJECTS\CoDstructor/src/cod/main.cpp:4: first defined here
obj/cod/main.o:main.cpp:(.text+0x2a): undefined reference to `lol()'
obj/cod/types.o:main.cpp:(.text+0x2a): undefined reference to `lol()'
collect2.exe: error: ld returned 1 exit status

Я искал почти два дня, как решить эти ошибки.

Для первого (несколько определений main): я уже проверил переменную OBJS, и она содержит и связывает только один раз main.o. И почему в первой строке написано obj/cod/types.o? В types.hpp/types.cpp нет главного.

Вторая ошибка (неопределенная ссылка на lol()): почему ссылка не определена, но вместо этого не дает ошибки компилятора?

Третья ошибка (каждый раз пересобирает все, а не только измененные или вместо поиска файлов зависимостей .d)

Я использую последнюю сборку MinGW32 (g++ 4.9.1) и последнюю версию MSYS (make).

Что я здесь делаю неправильно?


person nonsensation    schedule 01.08.2014    source источник
comment
Что такое неудачная команда компиляции?   -  person hyde    schedule 01.08.2014
comment
Я использую превосходную сборку: { "cmd":["make"], "path": "D:/DEV/MinGW/w32/mingw32/bin/;D:/DEV/msys/1.0/bin", "shell": true, }   -  person nonsensation    schedule 01.08.2014


Ответы (3)


Ваша ключевая проблема здесь:

$(OBJS): $(SOURCES)
   @mkdir -p $(@D)
   $(CXX) -MMD -MP $(CXXFLAGS) -c $< -o $@

У этого правила есть две проблемы.

  1. У вас есть несколько объектных файлов. Первое действие не сработает, если вы выходите за рамки одного подкаталога в каталоге src.

  2. Второе действие компилирует ваш src/cod/main.cpp дважды, один раз в obj/cod/main.o, а затем в obj/cod/types.o. Это потому, что $< — это первый элемент в списке зависимостей, а этот первый элемент — src/cod/main.cpp.

Вторую проблему решить легче, чем первую. Вам нужно правило шаблона:

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
   $(CXX) -MMD -MP $(CXXFLAGS) -c $< -o $@

Теперь о решении первой проблемы. Что делать, если у вас есть несколько исходных каталогов, каждый из которых является подкаталогом вашего каталога src? Вы хотите создать соответствующий подкаталог obj для каждого из них. Также обратите внимание, что есть каталог, который вы не создаете, каталог bin. Первое, что нужно сделать, это создать список каталогов, которые вам нужно создать.

MKDIRS = $(sort $(foreach i,$(OBJS),$(dir $i)))
MKDIRS += bin

Тогда вам нужно правило, чтобы сделать их. Начнем просто:

mkdirs:
   mkdir -p $(MKDIRS)

Однако это проблематично. Вы получите сообщения об ошибках от mkdir, и сборка остановится, если какой-либо из этих каталогов уже существует. Нам нужно создавать эти каталоги только в том случае, если они не существуют. Make предоставляет инструменты для фильтрации этого списка только по несуществующим каталогам, но я бы не стал этого делать. Для меня лучше использовать оболочку для принятия некоторых решений:

mkdirs:
   @sh -c \
     'for d in $(MKDIRS); do \
        if [ ! -d $$d ]; then echo mkdir -p $$d; mkdir -p $$d; fi \
      done'

Теперь нам нужно добавить это правило в качестве зависимости.

$(RELEASE_TARGET): mkdirs $(BIN_DIR)/$(APP_NAME)
   @echo Release built

$(DEBUG_TARGET): mkdirs $(BIN_DIR)/$(APP_NAME)
   @echo Debug built

Обратите внимание, что я сделал три вещи для этих целей.

  1. Я добавил цель mkdirs.

  2. Я удалил цель clean. Вы действительно не хотите делать это здесь. Это противоречит цели отдельных компиляций. Когда у вас есть сотни исходных файлов и вы вносите изменения в один из них, вы просто хотите перекомпилировать этот исходный файл, а затем пересобрать исполняемый файл из сотен уже существующих объектных файлов. Не выполняйте очистку сразу после сборки исполняемого файла! Вы всегда можете сделать make clean из командной строки, если считаете необходимым это сделать.

  3. Я изменил сообщения. Эти сообщения будут выдаваться после того, как зависимости будут удовлетворены. Они будут последним, что ты увидишь. Чтобы получать сообщения до начала действия, проще всего создать фальшивую цель, которая печатает желаемое сообщение.

Пара заключительных замечаний:

Обратите внимание, что mkdirs — фальшивая цель (как и all). Лучше всего добавить его в свой список .PHONY, а этот список лучше разместить заранее.

Наконец, цель $(DEBUG_TARGET): mkdirs $(BIN_DIR)/$(APP_NAME) — это бомба замедленного действия. То же самое касается $(RELEASE_TARGET). Однажды вы узнаете о параллельном make. Нет никакой гарантии, что каталоги будут созданы до того, как компилятор попытается скомпилировать код. Каталоги не будут существовать, и ба-бум, ваш make только что не удался. Обеспечение устойчивости вашего make-файла к параллельному выполнению — это вопрос другого вопроса об обмене стеками.

person David Hammen    schedule 01.08.2014
comment
Большое спасибо! Я последовал вашему объяснению, и теперь это работает! - person nonsensation; 01.08.2014
comment
И спасибо за clean-исправление, оно было у меня при тестировании моего make-файла, чтобы посмотреть, делает ли он то, что я действительно хотел сделать. - person nonsensation; 01.08.2014
comment
@Serthy: я редактировал параллельно с вашим согласием и вашим комментарием. Я добавил последний абзац о параллельном make. Вы должны быть осторожны с зависимостями в параллельном make. Указание цели mkdir в качестве зависимости — один из наиболее распространенных способов сбоя параллельной сборки. - person David Hammen; 01.08.2014
comment
Благодарю вас! Интересно, есть ли/почему нет более простого способа заменить make. У меня есть простой исполняемый файл или пакетный файл (с более простым синтаксисом и меньшим количеством недостатков, как вы упомянули о параллельном построении), который просто выполняет необходимые действия. - person nonsensation; 01.08.2014

Ваше правило $(OBJS): $(SOURCES) не то, что вы думаете. В результате вы создаете и main.o, и types.o из одного и того же файла main.cpp (параметр $< в командной строке). Следовательно, у вас есть два идентичных файла, которые конфликтуют, а types.cpp даже не был создан.

Правильным правилом будет $(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp

person keltar    schedule 01.08.2014
comment
Спасибо за объяснение. Однако при использовании $(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp используются только файлы в верхних каталогах, а подкаталоги и т. д. игнорируются, не так ли? - person nonsensation; 01.08.2014

По памяти, документы не проверял, проблема в следующем:

$(OBJS): $(SOURCES)
    @mkdir -p $(@D)
    $(CXX) -MMD -MP $(CXXFLAGS) -c $< -o $@

Это означает, что каждый объектный файл зависит от всех источников. И затем команда компиляции использует $<, что, я думаю, означает первую зависимость. Таким образом, вы компилируете как types.o, так и main.o из main.cpp (который, я полагаю, является первым из $(SOURCES)).


Одним из решений может быть использование шаблонного правила, например:

 %.o : %.c
     @mkdir -p $(@D)
     $(CXX) -MMD -MP $(CXXFLAGS) -c $< -o $@

Объектные файлы уже требуются вашим правилом $(BIN_DIR)/$(APP_NAME): $(OBJS), и это правило шаблона сообщит make, как их создавать.

person hyde    schedule 01.08.2014
comment
Спасибо, ваш ответ также очень ценен! - person nonsensation; 01.08.2014