Как make файлом собирать программы в другой директории

Обновлено: 06.07.2024

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

Но для более грамотной работы с проектом существуют сценарии, работа с которыми осуществляется в утилите Make.

Утилита Make работает с файлами сценариев, в которых описывается, какие цели мы перед собой ставим при сборке нашего проекта, а также то, каким путём мы этих целей будем добиваться. Также в файлах сценариев поддерживается очень много всего, что описать в рамках одного занятия невозможно. Поэтому в данном уроке мы проведём только очень краткое знакомство с написанием данных сценариев, создадим такой сценарий, который поможет нам собрать наш проект и увидим, что изменилось по сравнению с использованием командных файлов для сборки проектов, какие у нас появились новые возможности благодаря использованию утилиты Make.

Так как мы работаем с системой сборки MinGW, основанной на системе GNU, соответственно, и утилита Make будет также из этой системы.

Авторами GNU make являются Richard Stallman и Roland McGrath. Начиная с версии 3.76, разработкой программы руководит Paul D. Smith.

В файле сценария обязательно должно присутствовать хотя бы одно правило.

Правило состоит из цели, зависимостей или пререквизитов, обязательных для достижения цели, а также одной или нескольких команд, которые для достижения данной цели будут выполняться.


Команды перечисляются каждая с новой строки. Перед каждой такой командой мы используем табуляцию. Без этого сценарий работать не будет.

Когда мы запускаем утилиту make с командной строки, то помимо имени файла с ключом мы можем указать имя цели, которую мы хотим выполнить в сценарии. Если не ввести имя цели, то выполнится либо цель с именем all либо самая первая в файле сценария цель.

В качестве пререквизитов (зависимостей) как правило используются другие цели, представляющие собой, как правило, имена файлов. Также могут использоваться и фиктивные цели. Но об этом не в данном уроке.

Чтобы было немного понятнее, давайте поработаем всё-таки с утилитой Make на практике.

Создадим проект, как и прежде, из проекта прошлого занятия с именем MYPROG19 и присвоим ему имя MYPROG20.

Файл build.cmd можно будет теперь удалить. clean.cmd пока оставим.

Все лишние файлы из папки с нашей программой должны быть удалены, должны остаться только файлы с исходными текстами. Если это не так, то дадим команду clean.

Статья будет интересная прежде всего изучающим программирование на C/C++ в UNIX-подобных системах от самых корней, без использования IDE.

Компилировать проект ручками — занятие весьма утомительное, особенно когда исходных файлов становится больше одного, и для каждого из них надо каждый раз набивать команды компиляции и линковки. Но не все так плохо. Сейчас мы будем учиться создавать и использовать Мейкфайлы. Makefile — это набор инструкций для программы make, которая помогает собирать программный проект буквально в одно касание.

Для практики понадобится создать микроскопический проект а-ля Hello World из четырех файлов в одном каталоге:

Все скопом можно скачать отсюда
Автор использовал язык C++, знать который совсем не обязательно, и компилятор g++ из gcc. Любой другой компилятор скорее всего тоже подойдет. Файлы слегка подправлены, чтобы собирались gcc 4.7.1

Программа make

Если запустить
make
то программа попытается найти файл с именем по умолчание Makefile в текущем каталоге и выполнить инструкции из него. Если в текущем каталоге есть несколько мейкфайлов, то можно указать на нужный вот таким образом:
make -f MyMakefile
Есть еще множество других параметров, нам пока не нужных. О них можно узнать в ман-странице.

Процесс сборки

Компилятор берет файлы с исходным кодом и получает из них объектные файлы. Затем линковщик берет объектные файлы и получает из них исполняемый файл. Сборка = компиляция + линковка.

Компиляция руками

Самый простой способ собрать программу:
g++ main.cpp hello.cpp factorial.cpp -o hello
Каждый раз набирать такое неудобно, поэтому будем автоматизировать.

Самый простой Мейкфайл

В нем должны быть такие части:

Для нашего примера мейкфайл будет выглядеть так:

Обратите внимание, что строка с командой должна начинаться с табуляции! Сохраните это под именем Makefile-1 в каталоге с проектом и запустите сборку командой make -f Makefile-1
В первом примере цель называется all . Это цель по умолчанию для мейкфайла, которая будет выполняться, если никакая другая цель не указана явно. Также у этой цели в этом примере нет никаких зависимостей, так что make сразу приступает к выполнению нужной команды. А команда в свою очередь запускает компилятор.

Использование зависимостей

Использовать несколько целей в одном мейкфайле полезно для больших проектов. Это связано с тем, что при изменении одного файла не понадобится пересобирать весь проект, а можно будет обойтись пересборкой только измененной части. Пример:

Это надо сохранить под именем Makefile-2 все в том же каталоге

Теперь у цели all есть только зависимость, но нет команды. В этом случае make при вызове последовательно выполнит все указанные в файле зависимости этой цели.
Еще добавилась новая цель clean . Она традиционно используется для быстрой очистки всех результатов сборки проекта. Очистка запускается так: make -f Makefile-2 clean

Использование переменных и комментариев

Переменные широко используются в мейкфайлах. Например, это удобный способ учесть возможность того, что проект будут собирать другим компилятором или с другими опциями.

Это Makefile-3
Переменные — очень удобная штука. Для их использования надо просто присвоить им значение до момента их использования. После этого можно подставлять их значение в нужное место вот таким способом: $(VAR)

Что делать дальше

После этого краткого инструктажа уже можно пробовать создавать простые мейкфайлы самостоятельно. Дальше надо читать серьезные учебники и руководства. Как финальный аккорд можно попробовать самостоятельно разобрать и осознать такой универсальный мейкфайл, который можно в два касания адаптировать под практически любой проект:

подскажите, какими командами можно добиться, чтобы объектные файлы создавались не в директории с makefile-ом, а в другой директории?

вот, допустим, делаем gcc -c hello.c и появляется объектный файл hello.o в директории с makefile. как сделать, чтобы появлялся hello1.o в директории ./objects ?

2,492 1 1 золотой знак 11 11 серебряных знаков 28 28 бронзовых знаков спасибо. только тяжко пока такие makefile из вашего примера читать

ввиду того, что это совершенно не «общепринятая практика», «командами» получить желаемое, насколько мне известно, невозможно. придётся в мэйкфайле к имени каждого файла дописывать нужный вам путь. пример такого изменения.

и компилятору тоже надо явно указывать путь:

что ориентировочно надо сделать:

для удобства манипуляций добавить (где-нибудь в начале файла) переменную, содержащую путь к каталогу с объектными файлами:

упоминания каждого объектного файла предварить этой переменной. т.е.:

и ещё для удобства я бы предложил дважды встречающееся перечисление всех объектных файлов тоже перенести в переменную. вместо:

тогда проще будет и префикс добавить «одним махом» ко всем этим именам:

эти две строки тоже где-нибудь в начале файла можно поместить.


66k 154 154 золотых знака 66 66 серебряных знаков 203 203 бронзовых знака

Данный способ позволяет ничего не менять в Makefile, но не всегда срабатывает. Текущий каталог оказывается другой, поэтому если есть подключаемые файлы с путём относительно него, иногда нужно компилятору указать пути их поиска. Например, если c-файл тоже является промежуточной целью, как при использовании lex и yacc .

В самом Makefile CFLAGS должно меняться с помощью += , чтобы сохранить оба присвоения.

Все новые файлы создаются в objects, но если в текущем каталоге уже есть объектные файлы и они новее исходников, они будут использованы для линковки. Чтобы конечные и промежуточные целевые файлы не искались по VPATH , можно использовать директиву vpath внутри Makefile для выборочного расширения поиска исходников. Предлагаю следующую заготовку для начала Makefile:

У меня есть проект, где структура каталогов, как это:

Как написать файл makefile, который был бы в part/src (или где бы то ни было), который мог бы частично комплировать/связывать исходные файлы c / C++?/ src ?

могу ли я сделать что-то вроде -Я$projectroot/часть1/Ница-я$projectroot/часть1/МКП-я$projectroot/часть2/ГРЦ .

Если это сработает, есть ли более простой способ сделать это. Я видел проекты, где есть makefile в каждом из соответствующая часть? папки. [в этом посте я использовал знак вопроса, как в синтаксисе bash]

традиционный способ-это Makefile в каждом из подкаталогов ( part1 , part2 , etc.) позволяет строить их самостоятельно. Далее, имейте Makefile в корневом каталоге проекта, который строит все. "Корень" Makefile будет выглядеть примерно так:

так как каждая строка в цели make выполняется в своей собственной оболочке, нет необходимости беспокоиться о переходе обратно в дерево каталогов или в другое справочники.

Я предлагаю взглянуть на GNU сделать ручной раздел 5.7; это очень полезно.

Если у вас есть код в одном подкаталоге, зависящий от кода в другом подкаталоге, вам, вероятно, лучше с одним makefile на верхнем уровне.

посмотреть Рекурсивный Сделать Считается Вредным для полного обоснования, но в основном вы хотите, чтобы иметь полную информацию, необходимую для принятия решения о том, нужно ли перестраивать файл, и у него не будет этого, если вы расскажете ему только о трети своего проекта.

ссылка выше, кажется, не достижимый. Тот же документ доступен здесь:

опция VPATH может пригодиться, которая говорит, какие каталоги искать для исходного кода. Однако вам все равно понадобится опция-I для каждого пути включения. Пример:

это автоматически найдет соответствующий partXapi.cpp файлы в любом из vPath указанных каталогов и скомпилировать их. Однако это более полезно, когда каталог src разбит на подкаталоги. Для того, что вы описываете, как говорили другие, вам, вероятно, лучше с makefile для каждой части, особенно если каждая часть может стоять самостоятельно.

вы можете добавить правила в свой корневой файл Makefile для компиляции необходимых файлов cpp в других каталогах. Пример Makefile ниже должен быть хорошим началом в получении вас туда, где вы хотите быть.

если источники распространяются во многих папках, и имеет смысл иметь отдельные файлы Makefile, как предлагалось ранее, рекурсивный make-хороший подход, но для небольших проектов мне легче перечислить все исходные файлы в make-файл С их относительным путем к make-файл такой:

я могу установить VPATH таким образом:

затем я строю объекты:

и построение вывода еще проще:

можно сделать VPATH поколение автоматизирован:

или используя тот факт, что sort удаляет дубликаты (хотя это не важно):

Я думаю, что лучше указать, что использование Make (рекурсивное или нет) - это то, чего обычно вы можете избежать, потому что по сравнению с сегодняшними инструментами трудно учиться, поддерживать и масштабировать.

Это замечательный инструмент, но его прямое использование следует считать устаревшим в 2010+.

Если, конечно, вы не работаете в специальной среде, т. е. с устаревшим проектом и т. д.

используйте IDE,CMake или, если вы тяжело сердцевина, то Autotools.

(отредактировано из-за downvotes, ty Honza для указания)

в parentDir есть куча каталогов с исходными файлами в них: dirA, dirB, dirC. Различные файлы зависят от объектных файлов в других каталогах, поэтому я хотел иметь возможность сделать один файл из одного каталога и сделать эту зависимость, вызвав makefile, связанный с этой зависимостью.

по существу, я сделал один Makefile в parentDir, который имел (среди многого другого) общее правило, похожее на RC's:

каждый подкаталог включал этот makefile верхнего уровня, чтобы наследовать это общее правило. В файле Makefile каждого подкаталога я написал пользовательское правило для каждого файла, чтобы отслеживать все, от чего зависит каждый отдельный файл.

всякий раз, когда мне нужно было сделать файл, я использовал (по существу) это правило для рекурсивного создания любого/всех зависимости. Прекрасно!

Примечание: существует утилита под названием "makepp", что, кажется, делает эту задачу еще более интуитивно, но ради переносимости и не в зависимости от другого инструмента, я решил сделать это таким образом.

В этой главе я описываю свой способ построения make-файлов для сборки проектов с использование программы GNU Make и компилятора GCC (GNU Compiler Collection) . Предполагается, что вы хорошо знакомы с утилитой GNU Make. Если это не так, то прочтите сначала главу 2 - "GNU Make" .

1.1. Пример проекта

1.2. "Традиционный" способ построения make-файлов

  • example_1-traditional /
    • main.cpp
    • main.h
    • Editor.cpp
    • Editor.h
    • TextLine.cpp
    • TextLine.h
    • Editor.h

    Предполагается, что для компиляции программы используется компилятор GCC, и объектные файлы имеют расширение ".o". Файл Editor.h выглядит так:

    • Требуется "явно" перечислять все объектные файлы, из которых компонуется программа
    • Требуется "явно" перечислять, от каких именно заголовочных файлов зависит тот или иной объектный файл
    • Исполняемый файл программы помещается в "текущую" директорию. Если мне нужно иметь несколько различных вариантов программы (например, отладочный и рабочий), то каждый раз при переходе от одного варианта к другому требуется полная перекомпиляция программы во избежание нежелательного "смешивания" разных версий объектных файлов.

    Видно, что традиционный способ построения make-файлов далек от идеала. Единственно чем этот способ может быть удобен - своей "совместимостью". По-видимому, с таким make-файлом будут нормально работать даже самые "древние" или "экзотические" версии make (например, nmake фирмы Microsoft). Если подобная "совместимость" не нужна, то можно сильно облегчить себе жизнь, воспользовавшись широкими возможностями утилиты GNU Make. Попробуем избавиться от недостатков "традиционного" подхода.

    1.3. Автоматическое построение списка объектных файлов

    "Ручное" перечисление всех объектных файлов, входящих в программу - достаточно нудная работа, которая, к счастью, может быть автоматизирована. Разумеется "простой трюк" вроде:

    • Получить список всех файлов с исходным текстом программы (всех файлов с расширением cpp). Для этого можно использовать функцию wildcard .
    • Преобразовать список исходных файлов в список объектных файлов (заменить расширение ".cpp" на расширение ".o"). Для этого можно воспользоваться функцией patsubst .
    • example_2-auto_obj /
      • main.cpp
      • main.h
      • Editor.cpp
      • Editor.h
      • TextLine.cpp
      • TextLine.h
      • makefile

      Файл Editor.h теперь выглядит так:

      Список объектных файлов программы строится автоматически. Сначала с помощью функции wildcard получается список всех файлов с расширением ".cpp", находящихся в директории проекта. Затем, с помощью функции patsubst , полученный таким образом список исходных файлов, преобразуется в список объектных файлов. Make-файл теперь стал более универсальным - с небольшими изменениями его можно использовать для сборки разных программ.

      1.4. Автоматическое построение зависимостей от заголовочных файлов

      "Ручное" перечисления зависимостей объектных файлов от заголовочных файлов - занятие еще более утомительное и неприятное чем "ручное" перечисление объектных файлов. Указывать такие зависимости обязательно нужно - в процессе разработки программы заголовочные файлы могут меняться довольно часто (описания классов, например, традиционно размещаются в заголовочных файлах). Если не указывать зависимости объектных файлов от соответствующих заголовочных файлов, то может сложиться ситуация, когда разные объектные файлы программы будут скомпилированы с использованием разных версии одного и того же заголовочного файла. А это, в свою очередь, может привести к частичной или полной потере работоспособности собранной программы.

      Утилита GNU Make не сможет самостоятельно построить список зависимостей, поскольку для этого придется "заглядывать" внутрь файлов с исходным текстом - а это, разумеется, лежит уже за пределами ее "компетенции". К счастью, трудоемкий процесс построения зависимостей можно автоматизировать, если воспользоваться помощью компилятора GCC. Для совместной работы с make компилятор GCC имеет несколько опций:

      • При изменении какого-либо из исходных файлов будет построен заново лишь один соответствующий ему файл зависимостей
      • Построение файлов зависимостей происходит "параллельно" с основной работой компилятора и практически не отражается на времени компиляции

      После того как файлы зависимостей сформированы, нужно сделать их доступными утилите make. Этого можно добиться с помощью директивы include.

      Обратите внимание на использование функции wildcard . Конструкция

      • example_3-auto_depend /
        • main.cpp
        • main.h
        • Editor.cpp
        • Editor.h
        • TextLine.cpp
        • TextLine.h
        • makefile

        Вот как выглядит Editor.h из этого примера:

        • example_3-auto_depend /
          • iEdit
          • main.cpp
          • main.h
          • main.o
          • main.d
          • Editor.cpp
          • Editor.o
          • Editor.d
          • Editor.h
          • TextLine.cpp
          • TextLine.o
          • TextLine.d
          • TextLine.h
          • makefile

          Файлы с расширением ".d" - это сгенерированные компилятором GCC файлы зависимостей. Вот, например, как выглядит файл Editor.d, в котором перечислены зависимости для файла Editor.cpp:

          Теперь при изменении любого из файлов - Editor.cpp, Editor.hили TextLine.h, файл Editor.cpp будет перекомпилирован для получения новой версии файла Editor.o.

          1.5. "Разнесение" файлов с исходными текстами по директориям

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

          • example_4-multidir /
            • main.cpp
            • main.h
            • Editor /
              • Editor.cpp
              • Editor.h
              • TextLine.cpp
              • TextLine.h

              Вот как выглядит Editor.h для этого примера:

              • Для хранения списка директорий с исходными текстами я завел отдельную переменную source_dirs, поскольку этот список понадобится указывать в нескольких местах.
              • Шаблон поиска для функции wildcard (переменная search_wildcard s) строится "динамически" исходя из списка директорий source_dirs
              • Используется переменная VPATH для того, чтобы шаблонное правило могло искать файлы исходных текстов в указанном списке директорий
              • Компилятору разрешается искать заголовочные файлы во всех директориях с исходными текстами. Для этого используется функция addprefix и флажок -I компилятора GCC.
              • При формировании списка объектных файлов, из имен исходных файлов "убирается" имя каталога, где они расположены (с помощью функции notdir)

              1.6. Сборка программы с разными параметрами компиляции

              • Все варианты программы собираются с помощью одного и того же make-файла.
              • Необходимые настройки компилятора "попадают" в make-файл через параметры, передаваемые программе make в командной строке.
              • example_5-multiconfig /
                • main.cpp
                • main.h
                • Editor /
                  • Editor.cpp
                  • Editor.h
                  • TextLine.cpp
                  • TextLine.h

                  Файлы make_debug и make_release - это командные файлы, используемые для сборки соответственно отладочной и рабочей версий программы. Вот, например, как выглядит командный файл make_release:

                  Обратите внимание, что строка со значением переменной compile_flags заключена в кавычки, так как она содержит пробелы. Командный файл make_debug выглядит аналогично:

                  Вот как выглядит Editor.h для этого примера:

                  Переменная compile_flags получает свое значение из командной строки и, далее, используется при компиляции исходных текстов. Для ускорения работы компилятора, к параметрам компиляции добавляется флажок -pipe. Обратите внимание на необходимость использования директивы override для изменения переменной compile_flags внутри make-файла.

                  1.7. "Разнесение" разных версий программы по отдельным директориям

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

                  • example_6-multiconfig-multidir /
                    • debug /
                    • release /
                    • main.cpp
                    • main.h
                    • Editor /
                      • Editor.cpp
                      • Editor.h
                      • TextLine.cpp
                      • TextLine.h

                      Главная сложность заключалась в том, чтобы заставить программу make помещать результаты работы в разные директории. Попробовав разные варианты, я пришел к выводу, что самый легкий путь - использование флажка --directory при вызове make. Этот флажок заставляет утилиту перед началом обработки make-файла, сделать каталог, указанный в командной строке, "текущим".

                      Вот, например, как выглядит командный файл make_release, собирающий рабочую версию программы (результаты компиляции помещается в каталог release):

                      Команда mkdir введена для удобства - если удалить каталог release, то при следующей сборке он будет создан заново. В случае "составного" имени каталога (например, bin/release) можно дополнительно использовать флажок -p. Флажок --directory заставляет make перед началом работы сделать указанную директорию release текущей. Флажок --Editor.h укажет программе make, где находится make-файл проекта. По отношению к "текущей" директории release, он будет располагаться в "родительском" каталоге.

                      Командный файл для сборки отладочного варианта программы (make_debug) выглядит аналогично. Различие только в имени директории, куда помещаются результаты компиляции (debug) и другом наборе флагов компиляции:

                      Вот окончательная версия make-файла для сборки "гипотического" проекта текстового редактора:

                      В этом окончательном варианте я "вынес" имя исполняемого файла программы в отдельную переменную program_name. Теперь для того чтобы адаптировать этот make-файл для сборки другой программы, в нем достаточно изменить всего лишь несколько первых строк.