Компиляция и компоновка

Опубликовано bigov - сб, 02/18/2017 - 13:52

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

Можно из командной строки (вручную) выполнять компиляцию и компоновку всех своих исходных файлов:

src\> c++ -c app.cpp -o app.o
src\> c++ -c lib.cpp -o lib.o
src\> c++ app.o lib.o -o app.exe

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

make

Для работы make требуется наличие в каталоге запуска файла управления сборкой с фиксированным именем "Makefile" или прямое указание на имя такого файла через ключ (-f). Эту утилиту удобно использовать, пока код сравнительно небольшой или есть желание тщательно разобраться и понять как "все это" работает. Если потратить время на изучение документации, то можно научиться самостоятельно создавать универсальные мэйк-файлы с большой долей автоматизации (файл: mswin.make):

#------------------------------------------------------------------------------------------
#
# Makefile for
#  - MINGW32-W64 on MS-Windows
#

APPCMD =app.exe

# автоматический отбор всех имен исходных файлов в катклоге "src"
# и в папке в внешними модулями "include"
NAMES =$(basename $(wildcard src/*.cpp)) $(basename $(wildcard src/*.c))
DEPS  =$(wildcard include/*.cpp)

# автоматическое формирование списка объектных файлов для их
# компиляции в выделенную папку ("obj")
OBJS  =$(patsubst %,obj/%,$(notdir $(subst : , , $(NAMES:=.o)))) \
        $(DEPS:.cpp=.o)

# настройка компилятора
CPPFLAGS =-DMINGW64 -mwindows -Wl,-subsystem,windows -fexceptions \
        -std=c++11 -Werror -Wall -Wextra -Wpedantic -Woverloaded-virtual \
        -Wctor-dtor-privacy -Wnon-virtual-dtor -Wconversion \
        -Winit-self -Wunreachable-code -Weffc++ -Iinclude

# список подключаемых библиотек
LDLIBS =-lmingw32 -lopengl32 -lglfw3 -lpthread -lfreetype -lpng16 -lz -lm -lgdi32

all: LINK_APP

# команда сборки приложения
LINK_APP: $(OBJS)
        c++ -o $(APPCMD) --static $(OBJS) $(LDLIBS)

# компиляция исходников из списка
obj/%.o: src/%.cpp
        c++ $(CPPFLAGS) -c $^ -o $@

# компиляция подключаемых библиотек из списка
include/%.o: include/%.cpp
        c++ $(CPPFLAGS) -c $^ -o $@

# команда очистки рабочих каталогов
clean:
        DEL obj/*.o $(APPCMD)

.PHONY: all main clean
#------------------------------------------------------------------------------------------

Теперь достаточно из командой строки в директории приложения набрать команду "make" (если используется MingGW то "mingw32-make.exe") и приложение будет скомпилировано и собрано. Команда "make clean" традиционно очистит рабочие папки, удалив все сгенерированные файлы. Этой командой пользуются достаточно часто, так как "make" при работе сравнивает время создания файла исходного кода со временем создания существующего объектного файла и не компилирует его повторно, если иходный код не менялся. Это позволяет значительно быстрее формировать тестовые сборки при отладке приложения - перекомпилируются только измененные файлы. Но фалы заголовков, если на них нет указаний, при этом не проверяются. Поэтому, если вы что-то поменяли в заголовочном файле не затронув основной, то лучше объектный файл данного модуля пересобрать, удалив старый. Или пересобрать все приложение, если что-то "пошло не так".

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

make -p
выводит список всех неявных параметров и переменных окружения со значениями
make -n
выводит сформированный вашим мэйк-файлом набор команд компиляции и компоновки без их реального выполнения. Бывает очень полезно посмотреть, когда надо выяснить где ошибка в файле управления.

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

Иногда часть трудностей удается по-быстрому обойти при помощи создания отдельного пакетного файла для управления работой make. Например, такого (файл: make.cmd):

#------------------------------------------------------------------------------------------
@ECHO OFF
ECHO.
SET "EXEC=DEBUG\app.exe"

IF "%1"=="clean" GOTO _clean

IF NOT EXIST DEBUG MKDIR DEBUG
IF EXIST "%EXEC%" DEL /F /Q /S "%EXEC%"

ECHO Сборка проекта
ECHO ---------------

mingw32-make.exe -f mswin.make
IF ERRORLEVEL 1 (
        ECHO ----------------
        ECHO ..ошибка сборки
        ECHO.
        pause
        GOTO _eof
)

MOVE /Y app.exe DEBUG\
CD DEBUG
CALL app.exe
IF ERRORLEVEL 1 pause
CD ..
GOTO _eof

:_clean
ECHO Очистка данных:
ECHO ---------------
DEL /F /Q /S "DEBUG\app.exe"
DEL /F /Q /S "obj\*"

:_eof
ECHO.
#------------------------------------------------------------------------------------------

Здесь мы собираем при помощи команды "mingw32-make.exe -f mswin.make" свою программу, копируем ее в каталог DEBUG и проверяем результат запуском полученной программы. Этакий небольшой "хук", потому что можно настроить и мэйк-файл чтобы он тоже и копировал и запускал результат и выводил комментарий. Но мне показалось, что вместо ввода в командной строке "mingw32-make.exe -f mswin.make" удобнее сделать такой файлик и запускать его коротко: make.

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

cmake

Спрос рождает предложение. Cmake - это, сильно упрощая, оболочка для make. Вобще, сmake умеет работать с другими компиляторами, кроме GCC, и не только это. Но сейчас нас интересует то, что он позволяет на основе собственных, сравнительно простых, конфигурационных файлов автоматически генерировать мэйк-файлы для сборки сложных проектов. Конечно, для его использования тоже надо почитать документацию, но уровнь гибкости управления компиляцией вашего проекта при использовании cmake вырастет на порядок. Для примера - файл конфигурации того-же проекта при помощи cmake (файл: CMakeLists.txt):

#------------------------------------------------------------------------------------------
cmake_minimum_required( VERSION 3.0 )    # Проверка версии CMake.
project( Cool Application )
include_directories( include deps ) # где искать заголовки
SET( CMAKE_BUILD_TYPE Debug ) # включить отладку

SET( CMAKE_CXX_FLAGS "--static -mwindows -Wl,-subsystem,windows\
 -fexceptions -std=c++14 -Werror -Wall -Wextra -Wpedantic\
 -Woverloaded-virtual -Wctor-dtor-privacy -Wnon-virtual-dtor -Wconversion\
 -Winit-self -Wunreachable-code -Weffc++ -Iinclude -Ideps"
)

# список исходников
file( GLOB SRC "src/*.cpp" "deps/gl_com_3.cpp" )

# Исполняемый файл приложения
add_executable( app ${SRC} )

# Сборка приложения - линковка с библиотеками
SET( LIBS mingw32 opengl32 glfw3 pthread freetype png16 z m gdi32 )
target_link_libraries( app ${LIBS} )
#------------------------------------------------------------------------------------------

Для сборки проекта надо создать отдельную директорию для работы и запустить из нее команду

 debug\ > cmake _ПУТЬ_К_ПАПКЕ_ПРОЕКТА_  -G "MinGW Makefiles"

Которая сгенерирует набор мэйк-файлов для управления сборкой. После этого запустить make, или cmake c опцией сборки:

 debug\ > cmake --build .

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

 

Теги