Сообщений 23    Оценка 156        Оценить  
Система Orphus

Сборка cpp-проектов с помощью nmake

Автор: Денис Жданов
ATG
Опубликовано: 31.05.2006
Исправлено: 10.12.2016
Версия текста: 1.2
Введение
Структура пакета сборки
Реализация сборки
Настройка make-файлов
Настройка скрипта
Заключение
Благодарности

Введение

Программные продукты можно собирать различными способами. Мне удобнее всего делать это с помощью 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-файлов

Структура

Make-файлы поддерживают директиву препроцессора !INCLUDE, аналогичную директиве include в C++. Воспользуемся этим для того, чтобы хранить все ключи в одном месте (назовем этот файл, например, common.mak).

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

makefiles list
./config
     |
     |_common.mak
     |
     |_dll_debug.mak
     |
     |_dll_release.mak

Общяя иформация о make-файлах

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-файлов

С учетом всего вышесказанного мы можем теперь реализовать наши 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  
dll_debug.mak
          !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)/"
dll_release.mak
          !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
build.cmd

Про команды, использующиеся в скрипте (такие как @echo off или setlocal enabledelayedexpansion etc), можно почитать в статье Урок bat-аники.

Вместо создания make-файла, содержащего переменные COMPILER_FILE_SET и LINKER_FILE_SET, может возникнуть желание использовать переменные окружения. Это решение не подходит в общем случае, так как если проект содержит много *.cpp, их список не поместится в объем, доступный переменной окружения.

Заключение

Эта статья - практическое руководство, показывающее, как можно собрать срр-проект под Windows без помощи Visual Studio. Данную задачу можно было решить без участия сторонних утилит, то есть сделать все из командной строки. Я решил продемонстрировать вариант с nmake для того, чтобы дать общее представление о работе с этим инструментом.

Благодарности

Огромное спасибо жене как первому цензору, редактору и просто любимому человеку!

Большое спасибо Алексею Александрову за статью!

Огромное человеческое спасибо Коле Меркину за готовность делиться необъятным опытом!


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 23    Оценка 156        Оценить