Предположим, что, как и в моем случае, вы впервые столкнулись с необходимостью
минимизации телодвижения на пути от SVN исходников к NSIS инсталлеру с попутной автоинкрементацией версии проекта. В ручном же режиме это выглядит примерно так:
- Прописываем новую версию в соответствующем исходнике проекта. Нужно ли это для пользователей или в качестве диагностической информации – несущественно.
- Собираем проектные файлы, необходимые для setup.
- Обновляем версию внутри nsi-файла, поскольку используем ее в окнах на этапе установки и в имени результирующего setup-файла.
Уязвимость подобной практики обнаруживается при авралах, когда один или несколько из этих пунктов оказываются пропущенными, в результате чего у пользователя на руках остаются несогласованные между собой компоненты.
Не претендуя на новизну, предлагаемые ниже скрипты автоматизируют сборку C/C++ Visual Studio проектов практически в один клик и будут полезны, прежде всего, при одиночной разработке.
Предполагается использование формата A.B.C.D, где A и B меняются редко и вручную, а следующие два параметра подлежат обновлению при каждой release-компиляции:
- С – номер SVN ревизии;
- D – номер сборки, который инкрементируется.
Первый скрипт обновляет версию файла через специальный хедер, который, подключаясь к файлу ресурсов .rc, изменяет атрибуты компилируемого файла проекта. Второй скрипт использует их для генерации NSIS инсталлера, в имени которого будет отражена вся информация о текущей версии, например, MyApp-1.0.837.1.exe. Тем самым гарантируется автоматизация отмеченных выше этапов сборки.
Реализация автоинкрементации
Для получения номера SVN ревизии воспользуемся базовой утилитой SubWCRev.exe, а для автоинкремента номера сборки не обойтись без Pre-Build Event, как это было сделано, например, здесь или тут.
Создадим файл VersionInfo.h:
#ifndef __VERSION_INFO_H
#define __VERSION_INFO_H
#define APP_NAME "MyApp"
#define APP_VERSION 1,0,,0
#define APP_VERSION_S ""
#define APP_DATE ""
#endif
и подключим его в .rc файл (с указанием пути куда сохранили):
#include "../src/VersionInfo.h"
Изменяем версию итогового бинарника, которая понадобится позже для .nsi:
FILEVERSION APP_VERSION
PRODUCTVERSION APP_VERSION
В задачу скрипта VersionBuild.rb после каждого выполнения
C:Ruby193binruby VersionBuild.rb VersionInfo.h
входит изменение VersionInfo.h путем вставки специальных ключевых слов $WCREV$ и $WCNOW=$ из SubWCRev.exe:
#define APP_VERSION 1,0,$WCREV$,1
#define APP_VERSION_S "1.0.$WCREV$.1"
#define APP_DATE "$WCNOW=%d.%m.%Y %H:%M$"
#define APP_VERSION 1,0,$WCREV$,2
#define APP_VERSION_S "1.0.$WCREV$.2"
#define APP_DATE "$WCNOW=%d.%m.%Y %H:%M$"
...
Последующий вызов
SubWCRev.exe .. VersionInfo.h VersionInfo.h
подставит актуальные значения номера SVN ревизии и даты, например
#define APP_VERSION 1,0,29,2
#define APP_VERSION_S "1.0.29.2"
#define APP_DATE "01.11.2012 20:09"
При реализации VersionBuild.rb использованы регулярные выражения так, чтобы сохранялось форматирование (пробелы/табуляции):
FNAME = ARGV[0]
file = File::read(FNAME)
ANY_IN_QUOTES = Regexp.new('"[^"]*"')
def replaceVersion(file)
#строка с макросом APP_VERSION
matcher = Regexp.new('APP_VERSIONs*[^rn]*')
line = file.match(matcher)[0]
old_ver = /(d+),(d+),(.*),(d+)/
line.match(old_ver)
new_ver = [$1, $2, "$WCREV$", $4.to_i + 1]
line.sub!(old_ver, new_ver.join(","))
file.sub!(matcher, line)
#строка с макросом APP_VERSION_S
matcher = Regexp.new('APP_VERSION_Ss*' + ANY_IN_QUOTES.source)
line = file.match(matcher)[0]
line.sub!(ANY_IN_QUOTES, '"' + new_ver.join(".") + '"')
file.sub!(matcher, line)
end
def replaceDate(file)
#строка с макросом APP_DATE
matcher = Regexp.new('APP_DATEs*' + ANY_IN_QUOTES.source)
date = file.match(matcher)[0]
date.sub!(ANY_IN_QUOTES, '"$WCNOW=%d.%m.%Y %H:%M$"')
file.sub!(matcher, date)
end
replaceVersion(file)
replaceDate(file)
File::write(FNAME, file)
Чтобы избежать увеличения времени компиляции в большом проекте рекомендуется не включать VersionInfo.h в хедеры проекта, а завести дополнительные прокси-файлы:
#include "Version.h"
#include "VersionInfo.h"
const char *Version()
{
return APP_VERSION_S;
}
const char *BuildDate()
{
return APP_DATE;
}
Реализация сборки setup
Ниже приведен стандартный NSIS файл для установки одного exe (находится в той же папке) с созданием ярлыков на рабочем столе и в Пуске.
!include "MUI.nsh"
;Раскоммментировать при необходимости ручной сборки
;!define APP_NAME "MyApp"
;!define MAJOR_VERSION "1"
;!define MINOR_VERSION "0"
;!define SVN_REVISION "29"
;!define BUILD_NUMBER "2"
;!define APP_VERSION "${MAJOR_VERSION}.${MINOR_VERSION}.${SVN_REVISION}.${BUILD_NUMBER}"
;!define SETUP_NAME "${APP_NAME}-${APP_VERSION}.exe"
Name ${SETUP_NAME}
OutFile ${SETUP_NAME}
InstallDir $PROGRAMFILES${APP_NAME}
VIProductVersion ${APP_VERSION}
VIAddVersionKey "ProductName" ${APP_NAME}
VIAddVersionKey "Comments" ""
VIAddVersionKey "CompanyName" ""
VIAddVersionKey "LegalTrademarks" ""
VIAddVersionKey "LegalCopyright" "© "
VIAddVersionKey "FileDescription" ${APP_NAME}
VIAddVersionKey "FileVersion" ${APP_VERSION}
!define MUI_ABORTWARNING
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH
!insertmacro MUI_LANGUAGE "Russian"
Section "${APP_NAME} (required)"
SetOutPath $INSTDIR
File "${APP_NAME}.exe"
WriteUninstaller "uninstall.exe"
SectionEnd
Section "Uninstall"
Delete "$INSTDIR*.*"
Delete "$SMPROGRAMS${APP_NAME}*.*"
Delete "$DESKTOP${APP_NAME}.lnk"
RMDir "$SMPROGRAMS${APP_NAME}"
RMDir "$INSTDIR"
SectionEnd
Section "Start Menu and Desktop Shortcuts"
CreateDirectory "$SMPROGRAMS${APP_NAME}"
CreateShortCut "$SMPROGRAMS${APP_NAME}Uninstall.lnk" "$INSTDIRuninstall.exe" "" "$INSTDIRuninstall.exe" 0
CreateShortCut "$DESKTOP${APP_NAME}.lnk" "$INSTDIR${APP_NAME}.exe"
SectionEnd
Причем вместо ручного задания констант !define мы используем передачу констант APP_NAME и APP_VERSION утилите makensis.exe. Этим занимается скрипт SetupBuild.rb, принимающий на вход имя скомпилированного бинарного файла:
C:Ruby193binruby SetupBuild.rb MyApp.exe
К примеру, если после компиляции MyApp.exe имел APP_VERSION_S = «1.0.29.2», то скрипт запросит строковую версию из атрибутов файла и в случае успеха создаст файл установки MyApp-1.0.29.2.exe, иначе будет открыт лог-файл от NSIS в блокноте.
Исходный код SetupBuild.rb:
NSIS_MAKE = '"C:Program Files (x86)NSISmakensis.exe"'
require 'win32ole'
def versionOf(fname)
fso = WIN32OLE.new("Scripting.FileSystemObject")
return fso.GetFileVersion(fname)
end
def alert(msg)
fso = WIN32OLE.new("WScript.Shell")
fso.Popup msg
end
SRC_FNAME = File::expand_path(ARGV[0])
APP_NAME = File::basename(SRC_FNAME, '.*')
params = Hash.new
params['APP_NAME'] = APP_NAME
params['APP_VERSION'] = versionOf(SRC_FNAME)
params['SETUP_NAME'] = "#{APP_NAME}-#{params['APP_VERSION']}.exe"
#префикс /D сразу перед параметром задает соответствующую константу в NSIS (аналог !define)
cmd = params.map{|key, val| "/D#{key}=#{val}"}.join(" ")
LOG_FNAME = "#{APP_NAME}_#{$0}.log"
r = system("#{NSIS_MAKE} #{cmd} #{APP_NAME}.nsi > #{LOG_FNAME}")
if r
File::delete(LOG_FNAME)
else
system('notepad', LOG_FNAME)
end
Использование
Тестовый SVN-проект на Visual Studio 2008 лежит здесь: https://ruby-nsis.googlecode.com/svn/trunk.
К установке требуются:
Для release в Project->Properties->Build Events->Pre-Build Event->Command Line задано
C:Ruby193binruby "..srcVersionBuild.rb" "..srcVersionInfo.h"
SubWCRev.exe .. "..srcVersionInfo.h" "..srcVersionInfo.h"
При запуске setupSetupBuild.bat c содержимым
C:Ruby193binruby SetupBuild.rb MyApp.exe
будет сгенерирован setup-файл MyApp-1.0.x.y.exe, где x — номер SVN версии, y — порядковый номер компиляции.
Если поместить SetupBuild.bat в Post-Build Event, инсталлер будет собираться при каждой перекомпиляции.
Автор: shtr