Простая автоматизация версионности и сборки C/C++ проекта на Ruby

в 11:34, , рубрики: c++, NSIS, ruby, svn, Visual Studio, Программирование, метки: , , , ,

Предположим, что, как и в моем случае, вы впервые столкнулись с необходимостью
минимизации телодвижения на пути от 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.

К установке требуются:

  • NSIS
  • Ruby (использовались пути установки по-умолчанию)

Для 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

Источник

Поделиться

* - обязательные к заполнению поля