Pull to refresh

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

Reading time6 min
Views9.7K
Предположим, что, как и в моем случае, вы впервые столкнулись с необходимостью
минимизации телодвижения на пути от 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:\Ruby193\bin\ruby 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_VERSION\s*[^\r\n]*') 
	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_S\s*' + 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_DATE\s*' + 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" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0
	CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${APP_NAME}.exe"
  
SectionEnd

Причем вместо ручного задания констант !define мы используем передачу констант APP_NAME и APP_VERSION утилите makensis.exe. Этим занимается скрипт SetupBuild.rb, принимающий на вход имя скомпилированного бинарного файла:
C:\Ruby193\bin\ruby 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)\NSIS\makensis.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 лежит здесь.

К установке требуются:
  • NSIS
  • Ruby (использовались пути установки по-умолчанию)

Для release в Project->Properties->Build Events->Pre-Build Event->Command Line задано
C:\Ruby193\bin\ruby "..\src\VersionBuild.rb" "..\src\VersionInfo.h"
SubWCRev.exe  .. "..\src\VersionInfo.h" "..\src\VersionInfo.h"

При запуске setup\SetupBuild.bat c содержимым
C:\Ruby193\bin\ruby SetupBuild.rb MyApp.exe

будет сгенерирован setup-файл MyApp-1.0.x.y.exe, где x — номер SVN версии, y — порядковый номер компиляции.

Если поместить SetupBuild.bat в Post-Build Event, инсталлер будет собираться при каждой перекомпиляции.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 22: ↑20 and ↓2+18
Comments7

Articles