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

Автор: Денис Жданов
ATG
Опубликовано: 31.05.2006
Версия текста: 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

        call :check_environmentrem Project configuration
set SRC_PATH=./src
set BUILD_PATH=./build
set CONFIG_PATH=./config
set PRODUCT_NAME=myAPI
set ARGS_FILE=args.mak

rem LIB это пременная окружения, в которой хранится путь к каталогу, в котором линкер будет искать
rem библиотеки для разрешения связывания
set LIB="%CPP_BUILDER_HOME%/static_data"
set PATH=%PATH%;%CPP_BUILDER_HOME%/dynamic_data

rem Поддерживаемые аргументы запуска.
set DLL_DEBUG_ARG=dll_debugset DLL_RELEASE_ARG=dll_release
set HELP_ARG=help
set DEFAULT_ARG=%DLL_DEBUG_ARG%

rem Проверяем, было ли задан аргумент при запуске. Если нет, будем выполнять DEFAULT_ARG.
set CURRENT_ARG=%1
if "" == "%CURRENT_ARG%" (
    set CURRENT_ARG=!DEFAULT_ARG!
)

rem Обработка аргументов запуска.
if %DLL_DEBUG_ARG% equ %CURRENT_ARG% (
    call :cleancall :build_makefile_argscall :build_dll_debug
) else if "%DLL_RELEASE_ARG%" equ "%CURRENT_ARG%" (
    call :cleancall :build_makefile_argscall :build_dll_release
) else if "%HELP_ARG%" equ "%CURRENT_ARG%" (
    call :usage
) else (
    echo Argument %CURRENT_ARG% is not supported.
    call :usage
)
exit
rem -------------------------------------------------------
:check_environment  if "" == "%CPP_BUILDER_HOME%" (
    echo CPP_BUILDER_HOME not defined. Exiting...
    exit
  )
exit /b %ERRORLEVEL%
rem -------------------------------------------------------

rem -------------------------------------------------------
:clean
rmdir /Q /S "%BUILD_PATH%" 2>nul
  mkdir "%BUILD_PATH%"
exit /b %ERRORLEVEL%
rem -------------------------------------------------------
rem -------------------------------------------------------
:build_makefile_args
call :clean_makefile_argscall :build_compiler_argscall :build_linker_argsexit /b %ERRORLEVEL% rem -------------------------------------------------------rem -------------------------------------------------------
:clean_makefile_argsdel "%CONFIG_PATH%\%ARGS_FILE%" 2>nul
exit /b %ERRORLEVEL%
rem -------------------------------------------------------
rem -------------------------------------------------------
:build_compiler_argsfor /R "%SRC_PATH%" %%i in ("*.cpp") do (
    set CURRENT_ARG=%%~dpi

    rem Меняем расширение на obj.
set TEMP=%%~nxi
    set TEMP=!TEMP:cpp=obj!

    set CURRENT_ARG=!CURRENT_ARG!!TEMP!
   
    rem Создаем в %CONFIG_PATH% файл с именем %ARGS_FILE% с
    rem содержимым вида:
    rem COMPILER_FILE_SET=./first_dir/first.cpp\
    rem     ./first_dir/second_dir/second.cpp\
    rem     etc
ifnot exist %CONFIG_PATH%/%ARGS_FILE% (
        echo COMPILER_FILE_SET="!CURRENT_ARG!"\>"%CONFIG_PATH%/%ARGS_FILE%"
    ) else if "" equ "!LAST_COMPILER_ARG!" (
        set LAST_COMPILER_ARG=!CURRENT_ARG!
    ) else (
        echo     "!CURRENT_ARG!"\>>"%CONFIG_PATH%/%ARGS_FILE%"
    )
  )

  if "" neq "%LAST_COMPILER_ARG%" (
      echo     "%LAST_COMPILER_ARG%">>"%CONFIG_PATH%/%ARGS_FILE%"
  )
  exit /b %ERRORLEVEL%
rem -------------------------------------------------------
rem -------------------------------------------------------
:build_linker_argsfor /R "%SRC_PATH%" %%i in ("*.cpp") do (
    rem Меняем расширение на obj.
set TEMP=%%~nxi
    set TEMP=!TEMP:cpp=obj!

    set CURRENT_ARG=%BUILD_PATH%/!TEMP!
   
    rem Дописываем в %ARGS_FILE% аргументы ликера.
if "" equ "!LINKER_FILE_SET_FLAG!" (
        set LINKER_FILE_SET_FLAG=TRUE
       echo LINKER_FILE_SET="!CURRENT_ARG!"\>>"%CONFIG_PATH%/%ARGS_FILE%"
    ) else if "" equ "!LAST_LINKER_ARG!" (
        set LAST_LINKER_ARG=!CURRENT_ARG!
    ) else (
        echo     "!CURRENT_ARG!"\>>"%CONFIG_PATH%/%ARGS_FILE%"
    )
  )

  if "" neq "%LAST_LINKER_ARG%" (
      echo     "%LAST_LINKER_ARG%">>"%CONFIG_PATH%/%ARGS_FILE%"
  )
  exit /b %ERRORLEVEL%
rem -------------------------------------------------------

rem -------------------------------------------------------
:build_dll_debug
echo Build started...
  "%CPP_BUILDER_HOME%/bin/nmake" /nologo /F "%CONFIG_PATH%/dll_debug.mak" && echo Build SUCCESSFUL && exit /b %ERRORLEVEL%
  echo Build FAILED
exit /b %ERRORLEVEL%
rem -------------------------------------------------------rem -------------------------------------------------------
:build_dll_release
echo Build started...
  "%CPP_BUILDER_HOME%/bin/nmake" /nologo /F "%CONFIG_PATH%/dll_release.mak" && echo Build SUCCESSFUL && exit /b %ERRORLEVEL%
  echo Build FAILED
exit /b %ERRORLEVEL%
rem -------------------------------------------------------rem -------------------------------------------------------
:usage
echo "Usage: build.cmd [%DLL_DEBUG_ARG% | %DLL_RELEASE_ARG%]. Default argument is %DEFAULT_ARG%"
exit /b %ERRORLEVEL%
rem -------------------------------------------------------endlocal

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

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

Заключение

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

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

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

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

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


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