Сообщений 23 Оценка 156 Оценить |
Программные продукты можно собирать различными способами. Мне удобнее всего делать это с помощью IDE, но бывает, что такой вариант неприемлем – иногда необходимо сделать модуль, который позволял бы собирать продукт по исходникам (мы говорим о коде на C++) и не требовал при этом установленной Visual Studio. Одним из вариантов решения такой задачи является использование утилиты nmake, разработанной Microsoft. В статье описана реализация этого подхода и дано общее представление о работе с nmake.
Есть набор фалов, необходимых для работы компилятора и линкера:
ПРИМЕЧАНИЕ Понятно, что имеет смысл выносить в общую часть только те файлы, которые реально используются большинством проектов (содержимое …/VisualStudio/vc7/ PlatformSDK etc). Бибилиотеки и заголовочные файлы, специфичные для какого-либо проекта, можно хранить в его каталоге. |
Т.к. эти части будут использоваться при сборке каждого срр-проекта, их можно вынести в отдельное место (в моем случае это папка с незамысловатым именем C:\buildmastering). Из раздумий о том, что бы еще улучшить в области дизайна, родилась светлая мысль разделить работу по приготовлению к сборке и настройку компилятора с линкером.
Таким образом, в структуре любого проекта есть:
Итого, имеем:
каталог, используемый всеми сборками:C:\buildmastering | |_bin // Содержит *.exe | |_include // Содержит *.h | |_static_data // Содержит *.lib и *.pdb | |_dynamic_data // Содержит *.dll |
.../current_project | |_build | |_config | |_src |
Так как сборка любого проекта должна иметь доступ к общему каталогу, вынесем путь к нему в переменную окружения. Назовем ее, например, CPP_BUILDER_HOME.
Make-файлы – это конфигурационные файлы для nmake. Имеют расширение *.mak. Мы будем использовать их для хранения настроек компилятора и линкера, а cmd-скрипт для всего остального.
Make-файлы поддерживают директиву препроцессора !INCLUDE, аналогичную директиве include в C++. Воспользуемся этим для того, чтобы хранить все ключи в одном месте (назовем этот файл, например, common.mak).
Для каждого из нужных сценариев сборки создадим свой make-файл. Тогда для проекта, который представляет собой, например, одну динамическую библиотеку (да, бывают и такие невеликие проекты) с возможностью выбора между debug и release версией получаем следующий набор make-файлов:
makefiles list./config | |_common.mak | |_dll_debug.mak | |_dll_release.mak |
Make-файл поддерживает переменные. Задаются они в виде
VARIABLE_NAME= VARIABLE_VALUE |
Получить значение переменной можно следующим образом:
FIRST_VARIABLE=SOME_VALUESECOND_VARIABLE=$(FIRST_VARIABLE) |
Аналогично можно получить значения переменных окружения.
Комментарии задаются строкой, начинающейся символом #.
Главное в make-файле – это набор targets. Target - это идентификатор, с которым может быть ассоциирована команда. Между targets можно устанавливать зависимости:
target1: #target1 command target2: #target2 command target0 : target1 target2 #target0 command |
В этом примере перед вызовом команды target0 сначала будут построены target1 и target2, то есть выполнены команды target1 и target2.
Возможно задавать правила о том, как из target с определенным расширением построить target с тем же именем, но другим расширением. Например, до компиляции имеем набор *.cpp файлов, после компиляции получаем набор *.obj. Если мы зададим общее правило, описывающее, как из любого *.cpp получить *.obj, не будет необходимости для каждого конкретного *.cpp создавать отдельный target с командой, заключающейся в компиляции этого *.cpp в *.obj.
Есть стандартный набор расширений, для которых можно строить правила перехода. Туда, в частности, входят и cpp, и obj.
Синтаксис правила перехода, например, *.cpp в *.obj, выглядит так:
.cpp.obj: command |
ПРИМЕЧАНИЕ К сожалению, nmake заставляет явно указывать файлы, с которыми мы хотим работать. То есть возможности make-файлов не позволяют сформулировать правило вида “Мне нужны все *.cpp из данного каталога и всех его подкаталогов”. Поэтому в данной реализации списки аргументов компилятора и линкера выносятся в отдельный make-файл, который генерируется скриптом запуска. Затем он включается с помощью препроцессора. |
С учетом всего вышесказанного мы можем теперь реализовать наши make-файлы
common.mak#настройка компилятора COMPILER=$(CPP_BUILDER_HOME)/bin/cl.exe COMPILER_DLL_RELEASE_FLAGS=/Ox /Og /D "WIN32" /D "_CONSOLE" /D "_WINDLL" /D "_MBCS" /D "_AFXDLL" /EHsc /MD /GS /W3 /nologo /c /Wp64 /TP COMPILER_DLL_DEBUG_FLAGS=/Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_WINDLL" /D "_MBCS" /Gm /EHsc /RTC1 /MTd /Fd"$(BUILD_PATH)/$(PRODUCT_NAME).pdb" /W3 /nologo /c /Wp64 /ZI /TP COMPILER_INCLUDES=/I"$(CPP_BUILDER_HOME)/include" /I"$(SRC_PATH)" #настройка линкера LINKER=$(CPP_BUILDER_HOME)/bin/link.exe LINKER_DLL_RELEASE_FLAGS=/NOLOGO /INCREMENTAL /SUBSYSTEM:console /MACHINE:X86 LINKER_DLL_DEBUG_FLAGS=/NOLOGO /INCREMENTAL /DEBUG /PDB:"$(BUILD_PATH)/$(PRODUCT_NAME).pdb" /SUBSYSTEM:console /MACHINE:X86 |
!INCLUDE common.mak #Make-файл, генерируемый скриптом и содержащий аргументы #компилятора(COMPILER_FILE_SET) и линкера(LINKER_FILE_SET). #Имя этого файла(ARGS_FILE) определяется в скрипте запуска. !INCLUDE $(ARGS_FILE) #Первый target в файле, значит, он будет выполняться при запуске nmake. #Команда этого target состоит в линковке набора *.obj, переданного в #переменной LINKER_FILE_SET. При этом используются ключи линкера, определенные #в переменной LINKER_DEBUG_FLAGS. Для того, чтобы нам было что линковать, #сначала надо это что-то получить. Для этого выставляем зависимость от #набора target c расширением obj. Так как определено правило #получения *.obj из *.cpp, для каждого target из списка зависимости будет #вызвана команда из правила .cpp.obj all: $(COMPILER_FILE_SET) "$(LINKER)" $(LINKER_DLL_DEBUG_FLAGS) -OUT:$(BUILD_PATH)/$(PRODUCT_NAME).dll /DLL $(LINKER_FILE_SET) #Правило построения *.obj из *.cpp. Выполняет компиляцию с флагами, #определенными в переменной COMPILER_DEBUG_ARGS. Здесь используется #служебная переменная $*. Она означает путь и имя текущего #target без расширения. Так как в это правило передаются target вида #*.obj, применение к нему выражения $*.cpp просто даст нам #путь и имя *.cpp файла, который должен быть откомпилирован. .cpp.obj: "$(COMPILER)" $(COMPILER_DLL_DEBUG_FLAGS) $(COMPILER_INCLUDES) $*.cpp /Fo"$(BUILD_PATH)/" |
!INCLUDE common.mak !INCLUDE $(ARGS_FILE) #Единственное отличие этого target от аналогичного из dll_debug.mak #состоит в том, что при линковке используются флаги, определенные в #переменной LINKER_DLL_RELEASE_FLAGS. all: $(COMPILER_FILE_SET) "$(LINKER)" $(LINKER_DLL_RELEASE_FLAGS) -OUT:$(BUILD_PATH)/$(PRODUCT_NAME).dll /DLL $(LINKER_FILE_SET) #Единственное отличие этого правила от аналогичного из dll_debug.mak #состоит в том, что при компиляции используются флаги, определенные в #переменной COMPILER_DLL_RELEASE_FLAGS. .cpp.obj: "$(COMPILER)" $(COMPILER_DLL_RELEASE_FLAGS) $(COMPILER_INCLUDES) $*.cpp /Fo"$(BUILD_PATH)/" |
Файл с именем ARGS_FILE, содержащий определения COMPILER_FILE_LIST и LINKER_FILE_LIST, а также переменные BUILD_PATH, PRODUCT_NAME и SRC_PATH создаются и инициализируются скриптом запуска. О них будет рассказано ниже. CPP_BUILDER_HOME – переменная окружения, хранящая путь к общему для всех сборок каталогу(см. выше). |
Задачи скрипта:
Сам файл скрипта обычно находится в корне проекта, т.е. в нашем примере он будет здесь:
…/current_project | ... |_build.cmd |
Про команды, использующиеся в скрипте (такие как @echo off или setlocal enabledelayedexpansion etc), можно почитать в статье Урок bat-аники. Вместо создания make-файла, содержащего переменные COMPILER_FILE_SET и LINKER_FILE_SET, может возникнуть желание использовать переменные окружения. Это решение не подходит в общем случае, так как если проект содержит много *.cpp, их список не поместится в объем, доступный переменной окружения. |
Эта статья - практическое руководство, показывающее, как можно собрать срр-проект под Windows без помощи Visual Studio. Данную задачу можно было решить без участия сторонних утилит, то есть сделать все из командной строки. Я решил продемонстрировать вариант с nmake для того, чтобы дать общее представление о работе с этим инструментом.
Огромное спасибо жене как первому цензору, редактору и просто любимому человеку!
Большое спасибо Алексею Александрову за статью!
Огромное человеческое спасибо Коле Меркину за готовность делиться необъятным опытом!
Сообщений 23 Оценка 156 Оценить |