Как не забыть зависимости в make / CMake? - PullRequest
0 голосов
/ 12 января 2019

Я новичок в C ++ и пытаюсь освоить системы сборки, такие как make / CMake. Исходя из Go, кажется, что существует постоянный риск того, что если вы забудете что-то сделать, ваши двоичные файлы станут устаревшими. В частности, я не могу найти лучший способ помнить о необходимости обновления зависимостей / предварительных требований в make / CMake. Я надеюсь, что упускаю что-то очевидное.

Например, предположим, у меня есть базовый make-файл, который просто компилирует main.cpp:

CFLAGS = -stdlib=libc++ -std=c++17

main: main.o
    clang++ $(CFLAGS) main.o -o main

main.o: main.cpp
    clang++ $(CFLAGS) -c main.cpp -o main.o

main.cpp:

#include <iostream>

int main() {
    std::cout << "Hello, world\n";
}

Пока все хорошо; make работает как положено. Но предположим, что у меня есть какая-то другая библиотека только для заголовков с именем cow.cpp:

#include <iostream>

namespace cow {
    void moo() {
        std::cout << "Moo!\n";
    }
}

И я решил позвонить moo() из main.cpp через `include" cow.cpp ":

#include <iostream>
#include "cow.cpp"

int main() {
    std::cout << "Hello, world\n";
    cow::moo();
}

Однако я забываю обновить зависимости для main.o в makefile. Эта ошибка не обнаруживается в течение очевидного периода тестирования при запуске make и повторном запуске двоичного файла ./main, поскольку вся библиотека cow.cpp находится непосредственно include d в main.cpp. Так что все выглядит хорошо, и Moo! распечатывается, как и ожидалось.

Но когда я изменяю cow.cpp на печать Bark! вместо Moo!, тогда запуск make ничего не делает, и теперь мой бинарный файл ./main устарел, а Moo! все еще печатается от ./main.

Мне очень любопытно услышать, как опытные разработчики C ++ избегают этой проблемы с гораздо более сложными кодами. Возможно, если вы заставите себя разделить каждый файл на заголовок и файл реализации, вы хотя бы сможете быстро исправить все такие ошибки? Это тоже не кажется пуленепробиваемым; поскольку заголовочные файлы иногда содержат некоторые встроенные реализации.

В моем примере вместо CMake используется make, но похоже, что CMake имеет ту же проблему перечисления зависимостей в target_link_libraries (хотя транзитивность немного помогает).

В качестве связанного вопроса: кажется, что очевидным решением для системы сборки является просто просмотр исходных файлов и определение зависимостей (она может просто перейти на один уровень и полагаться на CMake для обработки транзитивности). Есть ли причина, по которой это не работает? Есть ли система сборки, которая действительно делает это, или я должен написать свою собственную?

Спасибо!

1 Ответ

0 голосов
/ 12 января 2019

Прежде всего вам нужно сослаться на файл зависимостей в вашем Makefile.

Это можно сделать с помощью функции

SOURCES := $(wildcard *.cpp)
DEPENDS := $(patsubst %.cpp,%.d,$(SOURCES))

, который возьмет имя всех *.cpp файлов, заменит и добавит расширение *.d для обозначения вашей зависимости.

Тогда в вашем коде

-include $(DEPENDS)

- говорит Makefile не жаловаться, если файлы не существуют. Если они существуют, они будут включены и перекомпилированы ваши источники в соответствии с зависимостями.

Наконец, зависимости могут быть созданы автоматически с параметрами: -MMD -MP для правил для создания файла объектов. Здесь вы можете найти полное объяснение. Что генерирует зависимости это MMD; MP, чтобы избежать некоторых ошибок. Если вы хотите перекомпилировать при обновлении системных библиотек, используйте MD вместо MMD.

В вашем случае вы можете попробовать:

main.o: main.cpp
    clang++ $(CFLAGS) -MMD -MP -c main.cpp -o main.o

Если у вас есть больше файлов, лучше создать единое правило для создания объектных файлов. Что-то вроде:

%.o: %.cpp Makefile 
    clang++ $(CFLAGS) -MMD -MP -c $< -o $@

Вы также можете посмотреть на эти 2 великолепных ответа:

В вашем случае более подходящий Makefile должен выглядеть следующим образом (могут быть некоторые ошибки, но дайте мне знать):

CXX = clang++
CXXFLAGS = -stdlib=libc++ -std=c++17
WARNING := -Wall -Wextra

PROJDIR   := .
SOURCEDIR := $(PROJDIR)/
SOURCES   := $(wildcard $(SOURCEDIR)/*.cpp)
OBJDIR    := $(PROJDIR)/

OBJECTS := $(patsubst $(SOURCEDIR)/%.cpp,$(OBJDIR)/%.o,$(SOURCES))
DEPENDS := $(patsubst $(SOURCEDIR)/%.cpp,$(OBJDIR)/%.d,$(SOURCES))

# .PHONY means these rules get executed even if
# files of those names exist.
.PHONY: all clean

all: main

clean:
    $(RM) $(OBJECTS) $(DEPENDS) main

# Linking the executable from the object files
main: $(OBJECTS)
    $(CXX) $(WARNING) $(CXXFLAGS) $^ -o $@

#include your dependencies
-include $(DEPENDS)

#create OBJDIR if not existin (you should not need this)
$(OBJDIR):
    mkdir -p $(OBJDIR)

$(OBJDIR)/%.o: $(SOURCEDIR)/%.cpp Makefile | $(OBJDIR)
    $(CXX) $(WARNING) $(CXXFLAGS) -MMD -MP -c $< -o $@

РЕДАКТИРОВАТЬ, чтобы ответить на комментарии В качестве другого вопроса, есть ли проблема с переписыванием определения DEPENDS как просто DEPENDS := $(wildcard $(OBJDIR)/*.d)?

Хороший вопрос, мне потребовалось некоторое время, чтобы понять вашу точку зрения

С здесь

$(wildcard pattern…) Эта строка, используемая в любом месте make-файла, заменены разделенным пробелами списком имен существующих файлов, которые соответствует одному из заданных шаблонов имен файлов. Если имя файла не существует соответствует шаблону, то этот шаблон пропускается из выходных данных функция подстановки.

Итак, wildcard возвращает список имен файлов, соответствующих шаблону. patsubst действует на строки, его не волнует, что это за строки: он используется как способ создания имен файлов зависимостей, а не самих файлов. В примере Makefile, который я разместил, DEPENDS фактически используется в двух случаях: при очистке с помощью make clean и с include, поэтому в этом случае они оба работают, потому что вы не используете DEPENDS ни в одном правиле. Есть некоторые различия (я пытался бежать, и вы должны подтвердить). При DEPENDS := $(patsubst $(SOURCEDIR)/%.cpp,$(OBJDIR)/%.d,$(SOURCES)), если вы запустите make clean зависимостей *.d, у которых нет соответствующего *.cpp файла, он не будет удален, как при вашем изменении. Напротив, вы можете включить зависимости, не относящиеся к вашему *.cpp файлу.

Я задавал эти вопросы : посмотрим ответы.

Если файлы .d удаляются из-за неосторожности, но файлы .o остаются, значит, у нас проблемы. В исходном примере, если main.d удален, а затем cow.cpp впоследствии изменен, make не поймет, что ему нужно перекомпилировать main.o, и, таким образом, он никогда не создаст заново файл зависимостей. Есть ли способ дешево создать файлы .d без перекомпиляции объектных файлов? Если так, то мы могли бы воссоздать все /.d файлы в каждой команде make?

Хороший вопрос снова.

Да, вы правы. На самом деле это была моя ошибка. Это происходит из-за правила

main: main.o
    $(CXX) $(WARNING) $(CFLAGS) main.o -o main 

на самом деле должно было быть:

main: $(OBJECTS)
    $(CXX) $(WARNING) $(CXXFLAGS) $^ -o $@

, чтобы он был перекомпонован (исполняемый файл обновлен) всякий раз, когда один из объектов изменяется, и они будут меняться всякий раз, когда изменяется один их cpp файл.

Остается одна проблема: если вы удалите свои зависимости, но не объекты, и измените только один или несколько заголовочных файлов (но не исходные файлы), тогда ваша программа не будет обновлена.

Я исправил и предыдущую часть ответа.

РЕДАКТИРОВАТЬ 2 Для создания зависимостей вы также можете добавить новое правило в Makefile: здесь является примером.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...