실전! 지속적인 통합 12편: MSBuild 고급편 II

이 글은 월간 마이크로소프트웨어(일명 마소) 2008년 12월호에 기고한 글입니다. 물론 구성이나 내용 상의 차이가 있을 수 있습니다.

벌써 12월이다. 오늘은 첫눈까지 내렸다. 눈은 내리자마자 아스팔트 바닥에 녹아버려 흔적조차 없지만 한 해가 다 갔다는 생각마저 든다. 지속적인 통합 칼럼도 이로써 12회째가 됐다. 그 동안 C++, C#, C++/CLI 소스 코드의 빌드, 단위테스트, MSBuild 사용법 등을 알아봤다. 이번 편까지 습득하면 여러분은 어지간한 문제도 스스로 해결할 수 있는 실력을 갖추게 되리라 믿는다. 아쉽게도 테스트 적용범위, 코딩표준 강제 같은 주제를 다룰 시간이 없었지만, 기본 실력이 탄탄하면 나머지는 큰 문제가 아닐 것이다.

최재훈 | SK 아이미디어의 게임 서버 팀에서 일한다. 요즘은 스크립트 엔진을 개발하는 데 전념하며, 새로운 도전을 즐긴다. 직업 외적인 측면에선 배철수의 음악 캠프를 15년째 즐겨 듣고, U2가 최고의 밴드라 생각한다.

지난 10월과 11월엔 MSBuild의 기초와 응용을 다뤘다. 처음엔 MSBuild를 간단한 C# 프로젝트에 적용해보며 기본을 익혔고, 그 다음엔 MSBuildTasks라는 확장기능을 이용해 ODBC DSN을 자동으로 생성하는 사례를 살펴보고 멀티 코어 및 64비트 환경에 대해 알아봤다. 두 번 모두 C# 프로젝트를 기준으로 설명했는데 아무래도 복잡한 C++ 프로젝트를 처음부터 설명하려다간 지면이 모자랄 것이기 때문이었다. 이번 시간엔 대규모 C++ 프로젝트의 빌드 자동화를 염두에 두고 이야기를 해볼 생각이다. 특히 빌드 시간을 단축하여 피드백 주기를 조금이라도 줄이는 기법을 알아볼 생각이다.

예제 환경 구성하기

좀더 실전적인 환경을 갖춰야 이해하기 쉬울 것 같다. 그래서 실제로 돌아가는 오픈 소스 라이브러리를 하나 선택해 빌드 자동화를 해나갈 생각이다. 어떤 라이브러리가 좋을까 잠깐 생각해보고 UnitTest++를 예제로 쓰기로 정했다. 단위테스트 자동화를 다룰 때 여러 번 언급했지만 UnitTest++은 C++용 단위테스트 라이브러리다. 리눅스와 윈도우를 모두 지원한다. 무엇보다 이번 칼럼에 적합한 조건을 갖췄다. UnitTest++은 단위테스트를 포함하는데 명령창에서 실행할 수 있기에 빌드 자동화가 잘 되가는지 눈으로 보고 확인하기 좋기 때문이다.

우선 뒤이어 제시될 과정을 일일이 따라 하기 귀찮은 독자를 위해 모든 예제 코드를 웹에 공개했다는 걸 밝힌다. 다운로드 받아 압축을 풀면 된다. 단, 64비트 프로그래밍이나 MSBuild 프로그래밍을 해본 경험이 적다면 귀찮더라도 차근차근 해보길 바란다. 직접 해보면 알게 모르게 도움이 많이 된다.

이제 UnitTest++을 다운로드 받자. 현재로선 1.4 버전이 최신이다. 압축을 풀면 VC++용 프로젝트 파일과 솔루션 파일이 여러 개 있는데 Visual Studio 2008용은 없다. UnitTest++.vsnet2005.sln을 비주얼 스튜디오 2008에서 열면 알아서 변환해준다. 하지만 이렇게 하면 기존 구성이 망가지므로 우리는 UnitTest++.vsnet2008.sln 등의 파일을 따로 만든다.


TestUnitTest++.vsnet2005.vcproj ? TestUnitTest++.vsnet2008.vcproj
UnitTest++.vsnet2005.vcproj ? UnitTest++.vsnet2008.vcproj
UnitTest++.vsnet2005.sln ? UnitTest++.vsnet2008.sln
	

이렇게 파일을 복사했다면 이제 각 복사본을 메모장 같은 편집기에서 연다. 그리고 단어 바꾸기 기능(Ctrl+H)을 이용해 "2005"를 "2008"로 바꾼다. 이때 "모두 바꾸기"를 하지 말고 일일이 눈으로 확인하는 수고를 거쳐야 나중에 후회할 일이 없다. 다소 귀찮더라도 말이다.

이 작업까지 끝나면 비로소 비주얼 스튜디오 2008에서 UnitTest++.vsnet2008.sln 파일을 열어 솔루션 변환을 한다. 큰 문제 없이 알아서 변환해줄 것이다.

여기서 잠깐! 왜 이렇게 귀찮은 과정을 일일이 설명하는 것일까? 작년까지 비주얼 스튜디오 2005를 이용해 프로젝트를 진행했다. 그러나 새 VC++ 9.0에 탑재된 기능이 필요하단 판단이 섰고, 하나씩 이전해나가기로 결정했다. 나머지 개발자는 VS2005를 계속 쓰기로 했기 때문에 기존 프로젝트에 영향을 줘선 안 됐다. 그래서 이렇게 번거로운 절차를 거쳐 VS2005용 프로젝트와 VS2008용 프로젝트로 나누었다. 물론 이렇게 두 가지 구성을 모두 가져가면 향후 한쪽에서만 문제가 발생할 수 있다. 이 문제를 원천적으로 막긴 힘들지만, 두 프로젝트 구성을 빌드 자동화하면 문제가 생겼을 때 바로 피드백을 받을 수 있다. 코드 베이스가 커서 한번에 이전하기 힘든 경우라면 이와 같은 접근 방법을 고려해볼 만 하다.

참고 문서. VS2005에서 VS2008로 넘어갈 때 예기치 않은 문제가 발생할 수도 있다. 블로그에 개인적으로 겪은 상황을 적어놨으니 도움이 되길 바란다.

폴더 구성

폴더 구성의 중요성에 대해선 몇 번 언급했던 걸로 기억한다. 프로젝트 디렉터리를 자동으로 만들어주는 오픈 소스 프로그램이 있을 정도로 중요한 문제다. 이번엔 프로젝트를 분기할 일이 없으니 tags, branch, trunk의 구분은 두지 않는다. trunk만 있다고 가정하고 다음과 같은 폴더 구성을 취할 생각이다. 앞으로 이 구성은 프로젝트의 요구사항에 맞춰 조금씩 변할 것이다.


src
 - UnitTest++
  - Src
   - Posix
   - tests
   - Win32
vendor
	

UnitTest++에 x64 플랫폼 추가하기

VS2008용 솔루션이 준비됐으니 이제 빌드를 해보자. Win32|Debug 로 기본 설정되어 있을 것이다. TestUnitTest++.vsnet2008 를 시작 프로젝트로 잡고 실행하면 단위 테스트가 모두 성공했다는 메시지가 나온다. 모든 게 정상이라면 x64 플랫폼을 추가해보자. 기본적으로 UnitTest++은 Win32 플랫폼만 제공하지만 간단히 64비트 플랫폼을 추가할 수 있다.

우선 [구성 관리자 – 활성 솔루션 플랫폼]를 열어 <새로 만들기>를 선택한다. [새 플랫폼 입력 또는 선택] 드롭박스를 열면 x64 가 있다. x64를 선택하고 Win32 설정을 복사하게끔 설정한다. 그러면 x64|Debug와 x64|Release가 만들어진다. 이때 비주얼 스튜디오가 알아서 x64 빌드에 필요한 옵션을 자동으로 선택해준다. 의심스러우면 프로젝트의 등록정보를 확인하면 된다.

그림 1. x64 플랫폼 추가

그림 1. x64 플랫폼 추가 #1

그림 1. x64 플랫폼 추가 #2

이제 [일괄 빌드]를 골라 Win32|Debug, Win32|Release, x64|Debug, x64|Release를 한꺼번에 빌드해보고 하나하나 실행해보자. 별다른 이상 없이 단위 테스트가 성공할 것이다.

그림 2. 일괄 빌드

그림 2. 일괄 빌드

여기서 잠깐! 모든 소스 코드가 이렇게 간단히 플랫폼 변경이 되진 않는다. 특히 64비트에선 포인터의 크기가 32비트가 아닌 64비트(당연한 이야기지만)이기 때문에, 포인터 처리를 고민하지 않은 코드 때문에 문제가 발생하곤 한다. 특히 32비트 윈도우를 가진 개발자가 64비트용 애플리케이션을 개발할 때 이런 문제가 잦은데, 64비트 빌드 서버를 마련하여 이런 문제가 발생하자마자 대응할 수 있도록 환경을 꾸며야 한다.

MSBuild_x64.bat

지난 10월에 MSBuild_Win32.bat 를 소개했다. 이 배치 파일은 단순히 msbuild 바이너리의 래퍼다. 비주얼 스튜디오의 환경을 불러내는 일을 하지만 그 외엔 어려울 게 없다. 10월 칼럼과 달리 이번엔 x64 플랫폼을 집중적으로 다룰 것이니 64비트용 배치 파일을 만들자.


@echo off
SET PATHSAVED=%PATH% > _temp.txt
call "%VS90COMNTOOLS%\..\..\VC\vcvarsall.bat" x64 >> _temp.txt
"C:\WINDOWS\Microsoft.NET\Framework64\v3.5\msbuild.exe" %*
SET ERR_LEVEL=%errorlevel%
SET PATH=%PATHSAVED% >> _temp.txt
SET PATHSAVED=>> _temp.txt
exit /b %ERR_LEVEL%
	

이 배치 파일은 64비트용 비주얼 스튜디오 환경을 불러내고 64비트용 msbuild 바이너리를 호출할 뿐 하는 일은 기존 MSBuild_Win32.bat와 똑같다. 이 파일을 src/MSBuild_x64.bat에 저장하자

Win32용 빌드 스크립트

10월에 만든 msbuild.xml 템플릿을 가져다 간단한 UnitTest++.vsnet2008.sln 래퍼 스크립트를 만들자.

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">Win32</Platform>
	<BuildCondition>$(Configuration)|$(Platform)</BuildCondition>
</PropertyGroup>

<PropertyGroup Condition=" '$(BuildCondition)' == 'Debug|AnyCPU' ">
</PropertyGroup>

<PropertyGroup Condition=" '$(BuildCondition)' == 'Release|AnyCPU' ">
</PropertyGroup>

<!-- 특별한 문자열 -->
<PropertyGroup>
	<Semicolon>%3b</Semicolon>
</PropertyGroup>

<!-- 비주얼 스튜디오 2005 등 도구 바이너리 -->
<PropertyGroup>
	<DevEnv>devenv</DevEnv>
</PropertyGroup>

<ItemGroup>
	<ProjectReferences Include=".\UnitTest++\UnitTest++.vsnet2008.sln">
		<Configuration>$(Configuration)</Configuration>
		<Platform>$(Platform)</Platform>
	</ProjectReferences>
</ItemGroup>

<Target Name="Clean">
	<Message Text="타겟: Clean" />
</Target>

<Target Name="Build">
	<Message Text="타겟: Build" />
	<Message Text="빌드 조건: '$(BuildCondition)'" Importance="high" />

	<Exec Command="$(DevEnv) &quot;%(ProjectReferences.FullPath)&quot; /Build &quot;%(ProjectReferences.Configuration)|%(ProjectReferences.Platform)&quot;  " ContinueOnError="false" IgnoreExitCode="false" />
</Target>

<Target Name="Rebuild" DependsOnTargets="Clean; Build">
	<Message Text="타겟: Rebuild" />
</Target>
</Project>
	

10월과 달라진 점은 많지 않다. 우선 MSBuild 태스크를 쓰는 대신 Exec 태스크로 비주얼 스튜디오 바이너리를 직접 호출한다. 왜 이렇게 하는지는 전에 설명했는데 다시 요약하자면, C#일 땐 MSBuild 태스크를 C++일 땐 Exec 태스크를 쓰는 편이 좋다.

특수 문자

MSBuild엔 이스케이프해야 하는 문자열이 몇 개 있다. "를 &quot;로 표기하는 경우 등이 있는데 MSDN에서 "MSBuild, 예약된 XML 문자"와 "msbuild, 이스케이프할 특수 문자"를 찾으면 된다. 그런데 "예약된 XML 문자"는 어떤 문자로 교체하면 될지 MSDN에 적혀 있지만 "이스케이프할 특수 문자"는 그렇지 않다. 다행히 How To: Escape Special Characters in MSBuild라는 문서에 해결책이 제시되어 있다.

특수 문자 대신 %xx를 적으면 되는데 이때 xx는 해당 ASCII 문자의 16진수 값이다. ‘;’는 16진수로 3b이기 때문에 위의 예제 코드에서 <Semicolon>%3b</Semicolon>라고 정의했다.

x64용 빌드 스크립트

이번 시간엔 64비트 빌드를 할 것이니 기존 템플릿을 살짝 고쳐야 한다. 아래와 같이 고쳐보자.

<Choose>
	<When Condition=" '$(MSBuildBinPath)'=='%SystemRoot%\Microsoft.NET\Framework64\v2.0.50727' ">
		<PropertyGroup>
			<Is64Bit>true</Is64Bit>
			<Platform>x64</Platform>
		</PropertyGroup>
	</When>
	<Otherwise>
		<PropertyGroup>
			<Is64Bit>false</Is64Bit>
			<Platform>Win32</Platform>
		</PropertyGroup>
	</Otherwise>
</Choose>
	

만약 빌드 스크립트를 돌리는 msbuild 빌드 바이너리가 64비트용이라면 x64 플랫폼으로, 그렇지 않으면 Win32 플랫폼으로 설정한다. Is64Bit 프로퍼티는 매번 바이너리 경로를 비교하기 귀찮아서 만든 것에 불과하다. 없어도 되지만 있으면 좋다. 이제 빌드 스크립트가 준비됐다. 모든 빌드 구성이 잘 작동하는지 아래의 명령어를 쳐서 확인해보자.


MSBuild_Win32.bat msbuild.xml ./p:Configuration=Debug
MSBuild_Win32.bat msbuild.xml ./p:Configuration=Release
MSBuild_x64.bat msbuild.xml ./p:Configuration=Debug
MSBuild_x64.bat msbuild.xml ./p:Configuration=Release
	

똑똑한 빌드란?

대규모 C++ 프로젝트를 생각해보자. C++은 컴파일과 링크에 꽤 많은 시간을 쏟는 프로그래밍 언어다. 특히 Release 빌드시 최적화 옵션을 넣다 보면 한번 빌드하는데 1시간이 넘게 걸리기도 한다. 빌드 시간을 줄이는 여러 방법이 소개됐지만 빌드 마스터(그런 직책이 있던 없던)의 입장에선 다른 방법을 강구해봐야 한다.

처음 생각해낸 방법은 Rebuild를 안 하고 Build만 하는 것이다. 이렇게 하면 빌드 시간이 확실히 줄어든다. 하지만 Rebuild하지 않으면 빌드가 안 되는 경우가 있다. UnitTest++은 미리 컴파일된 헤더를 쓰지 않아서 괜찮겠지만 만약 쓴다고 하면 UnitTest++ 라이브러리가 많이 바뀌었을 때 TestUnitTest++ 프로젝트에서 오류가 날 것이다. 그렇다고 미리 컴파일된 헤더를 사용하지 않으면 그 때문에 빌드 시간이 증가할 테니 마땅한 수가 안 난다.

이 방법은 어떨까? 일단 Build만 하고 빌드가 깨졌을 때만 Rebuild하는 것이다. 빌드가 깨지면 CCTray에 빨간 불이 들어올 테니 그때 사람이 Force Build 버튼을 누른다. 이렇게 Froce Build 버튼을 누르면 Rebuild하게 하는 것이다. 나쁘지 않다. 하지만 더 나은 방법이 있지 않을까?

고민 끝에 생각해낸 방법은 생각보다 간단하다. 공유 라이브러리(이 경우엔 UnitTest++)가 바뀌었을 때만 Rebuild하고 그렇지 않은 땐 Build하는 것이다. 이것은 100% 자동화가 가능한 프로세스인데 이제부터 어떻게 하는지 다룰 것이다.

CI용 빌드 스크립트를 만들기 전에

CCNET용 빌드 스크립트, 그러니까 방금 언급한 똑똑한 빌드 스크립트를 만들기 전에 우선 UnitTest++의 폴더 구조를 바꿔야 한다. 새 폴더 구조는 다음과 같다. 단순히 tests 폴더를 UnitTest++ 코드와 분리시켰다. 이렇게 프로젝트 별로 소스 코드를 분리시켜야 똑똑한 빌드 스크립트를 만들기 쉽다.


UnitTest++
 - Src
  - Posix
  - Win32
 - tests
	

tests는 TestUnitTest++의 소스 코드를 담고 있는데, 파일의 경로가 바뀌었으니 소스 코드도 고쳐야 한다. TestUnitTest++.vsnet2008.vcproj 파일을 메모장에서 열고 "바꾸기" 기능을 활용해 다음과 같이 고치자.

.\src\tests\Main.cpp -> .\tests\Main.cpp
 #include "../UnitTest++.h" ? #include "../src/UnitTest++.h"
	

이제 일괄 빌드를 해서 실행해보자. 문제가 없으면 MSBuildTasks를 설치하자. 11월에 MSBuildTasks에 대해 다뤘으니 서브버전 저장소에 접근할 필요성이 있기 때문에 이 확장 기능을 설치한다는 정도만 밝히고 자세히 설명하진 않겠다. 이 예제에선 MSBuildTasks를 vendor 폴더 아래 둔다.


UnitTest++
 - Src
  - Posix
  - Win32
 - tests
vendor
 - MSBuildCommunityTasks
	

CI용 빌드 스크립트

CI용 빌드 스크립트라…… 이 말이 의미하는 바를 한번 생각해보길 바란다. 빌드 스크립트면 빌드 스크립트지 CI용 빌드 스크립트라니 무슨 말인가? 간단히 말해 우리가 여태 개발한 빌드 스크립트는 개발자용 빌드 스크립트다. 여태까지 개발자용 빌드 스크립트와 CI용 빌드 스크립트를 구분한 적이 없다. 구분하지 않는다고 하루 아침에 불행이 찾아오진 않는다. 그러니 이 기법에 집착할 이유는 없지만 구분하면 좋은 점이 많다. 무엇보다 역할을 나누면 유지보수하기가 쉽다. 빌드 스크립트 하나가 비대해지면 마우스 스크롤하기도, 코드를 이해하기도 힘들다. 개발자에게 필요 없는 코드를 마구 넣어서 누군가의 두통을 유발하고 원성 들을 이유는 없다. 스크립트를 나누는 게 어렵지도 않으니 말이다.

src/msbuild.xml과 나란히 src/msbuild-ci.xml 파일을 만들자.

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<Import Project="..\vendor\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

<Import Project="msbuild.xml"/>

<!-- 서브버전 -->
<PropertyGroup>
	<SvnUsername>계정 이름</SvnUsername>
	<SvnPassword>암호</SvnPassword>
	<SvnLocalRootPath>..</SvnLocalRootPath>
</PropertyGroup>

<!-- 공유 라이브러리의 로컬 경로 -->
<ItemGroup>
	<SharedLibrary Include="Item">
		<LocalPath>UnitTest++\src</LocalPath>
		<EscapedLocalPath>UnitTest-src</EscapedLocalPath>
	</SharedLibrary>
</ItemGroup>

<!-- Build할까? Rebuild할까? -->
<Target Name="DetermineBuildConditionForCI">
</Target>

<Target Name="SrcUpdate">
	<SvnUpdate Username="$(SvnUsername)"
		Password="$(SvnPassword)"
		LocalPath="$(SvnLocalRootPath)">
	</SvnUpdate>
</Target>

<Target Name="CleanForCI" DependsOnTargets="Clean">
	<Message Text="타겟: CleanForCI" />
</Target>

<Target Name="BuildForCI" DependsOnTargets="DetermineBuildConditionForCI; SrcUpdate ">
	<Message Text="CI용 빌드 타겟: $(BuildConditionForCI)" />

	<CallTarget Targets="$(BuildConditionForCI)" />
</Target>

</Project>
	

이 코드엔 DetermineBuildConditionForCI가 빠졌다. 똑똑한 빌드를 지원하는 주 기능인데 기본적으로 Build할 건지 Rebuild할 건지 여부를 $( BuildConditionForCI) 프로퍼티에 담아서 반환한다. 일단 그 정도만 알고 다른 부분을 보자.

처음엔 Import 태스크를 이용해 MSBuildTasks와 개발자용 빌드 스크립트(msbuild.xml)를 임포트한다. 이로써 MSBuildTasks와 msbuild.xml의 기능을 모두 쓸 수 있게 됐다. 뒤이어 서브버전 정보가 나온다. 똑똑하게 빌드하려면 SVN 기능이 필요하기 때문이다. 공유 라이브러리의 소스 코드가 바뀌면 Rebuild할 것이니 어떤 게 공유 라이브러리인지 알아야 한다. 그래서 SharedLibrary라는 ItemGroup을 추가했다. 일전에 설명했듯 ItemGroup은 일종의 배열이므로 필요하다면 경로를 더 추가해도 좋다. EspacedLocalPath는 단순히 경로 값의 별칭일 뿐이다. 차라리 Alias로 이름 지을 걸 그랬나 싶기도 하다.

CleanForCI는 단순히 개발자 빌드의 Clean 타겟을 호출할 뿐이다. BuildForCI는 DetermineBuildConditionForCI와 SrcUpdate를 차례대로 호출한다. 어떤 소스 코드가 바뀌었는지 확인한 후 소스 코드를 업데이트해야 하므로 이 순서를 반드시 지켜야 한다. 만약 똑똑한 빌드가 Build하기로 결정했다면 CallTarget이 개발자 빌드의 Build 타겟을 실행하고, Rebuild하기로 결정했다면 Rebuild 타겟을 실행한다. 이로써 똑똑한 빌드의 기본 틀이 완성됐다.

똑똑한 빌드

자, 이제 오늘의 핵심인 DetermineBuildConditionForCI 타겟의 구현을 살펴보자.

<Target Name="DetermineBuildConditionForCI">
	<!--
		일반 빌드인지 CC.NET가 명령한 ForceBuild인지에 알아낸다.
		ForceBuild라면 Rebuild한다.
	-->
	<CreateProperty
		Value="Build" Condition = " '$(CCNetBuildCondition)'=='IfModificationExists' ">
		<Output
			TaskParameter="Value"
			PropertyName="BuildConditionForCI" />
	</CreateProperty>
	<CreateProperty
		Value="Rebuild" Condition = " '$(CCNetBuildCondition)'!='IfModificationExists' ">
		<Output
			TaskParameter="Value"
			PropertyName="BuildConditionForCI" />
	</CreateProperty>

	<Message Text="CCNetBuildCondition = '$(CCNetBuildCondition)'" />

	<!-- 일반 빌드로 판명됐을지라도, 공유 라이브러리가 변경되었으면 Rebuild한다. -->
	<SvnInfo Username="$(SvnUsername)"
		Password="$(SvnPassword)"
		LocalPath="%(SharedLibrary.LocalPath)">
		<Output	TaskParameter="Revision" ItemName="LocalRevisionFor%(SharedLibrary.EscapedLocalPath)" />
		<Output TaskParameter="RepositoryPath" ItemName="RepositoryPathFor%(SharedLibrary.EscapedLocalPath)" />
	</SvnInfo>

	<Message Text="Local revision for local path '%(SharedLibrary.LocalPath)': @(LocalRevisionFor%(SharedLibrary.EscapedLocalPath))" />
	<Message Text="Repository path for local path '%(SharedLibrary.LocalPath)': @(RepositoryPathFor%(SharedLibrary.EscapedLocalPath))" />

	<SvnInfo Username="$(SvnUsername)"
		Password="$(SvnPassword)"
		RepositoryPath="@(RepositoryPathFor%(SharedLibrary.EscapedLocalPath))" >
		<Output	TaskParameter="Revision" ItemName="RepositoryRevisionFor%(SharedLibrary.EscapedLocalPath)" />
	</SvnInfo>

	<Message Text="Remote revision for repository path 'RepositoryRevisionFor%(SharedLibrary.EscapedLocalPath)': @(RepositoryRevisionFor%(SharedLibrary.EscapedLocalPath))" />

	<CreateProperty
		Value="Rebuild" Condition = " '@(LocalRevisionFor%(SharedLibrary.EscapedLocalPath))' != '@(RepositoryRevisionFor%(SharedLibrary.EscapedLocalPath))' ">
		<Output
			TaskParameter="Value"
			PropertyName="BuildConditionForCI" />
	</CreateProperty>

	<Message Text="BuildConditionForCI = $(BuildConditionForCI)" Importance="high"/>
</Target>
	

꽤 길고 복잡하다. 그러나 이 코드가 하는 일은 그리 복잡하지 않다. 우선 Force Build를 했는지 안 했는지 알아낸다. Force Build라면 $(BuildConditionForCI)의 값을 Rebuild로 설정한다. 여기서 핵심은 ‘$(CCNetBuildCondition)’!=’IfModificationExists’ 다. 이 조건이 만족하면 누군가 Force Build 버튼을 누른 것이다.

Force Build가 아니더라도 공유 라이브러리가 변경됐다면 Rebuild하는 편이 좋다. 우선 SvnInfo을 이용해 로컬 공유 라이브러리 코드의 리비전 번호를 알아낸다. 그리고 원격 저장소에 저장된 코드의 최신 리비전 번호를 알아낸다. 마지막 CreateProperty에서 로컬 리비전과 원격 리비전을 비교해보고 원격에 최신 코드가 올라온 경우엔 Rebuild하기로 결정한다.

CruiseControl .NET 설정하기

똑똑한 빌드가 가능하려면 ccnet.config를 고쳐야 한다.

<sourcecontrol type="svn">
	<executable>C:\Program Files (x86)\Subversion\bin\svn.exe</executable>
	<autoGetSource>false</autoGetSource>
	<timeout units="minutes">5</timeout>
	<username>계정 이름</username>
	<password>암호</password>
</sourcecontrol>

<tasks>
	<msbuild>
		<executable>MSBuild_x64.bat</executable>
		<workingDirectory>C:\src\</workingDirectory>
		<projectFile>msbuild-ci.xml</projectFile>
		<buildArgs>/noconsolelogger /v:m</buildArgs>
		<targets>BuildForCI</targets>
		<timeout>12000</timeout>
		<logger>ThoughtWorks.CruiseControl.MsBuild.XmlLogger,C:\Program Files (x86)\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MSBuild.dll</logger>
	</msbuild>
</tasks>
	

핵심적인 부분만 적었는데, 제일 중요한 건 autoGetSource다. 보통은 새 소스 코드를 SVN 저장소에서 가져오는 역할을 CCNET 맡는다. 그러나 이 경우는 그래선 안 된다. DetermineBuildConditionForCI가 로컬 소스 코드와 원격 소스 코드의 리비전을 비교한 후에 업데이트해야 하기 때문이다. 만약 빌드 스크립트가 실행되기 전에 CCNET이 소스 코드를 업데이트해버리면 “일반 빌드로 판명됐을지라도, 공유 라이브러리가 변경되었으면 Rebuild한다”는 주석 아랫 부분은 무용지물이 될 것이다.

끝마치는 말

1년에 걸쳐 지속적인 통합에 대해 알아봤다. C++은 가장 자동화하기 어려운 프로그래밍 언어 중에 하나다. 그럼에도 불구하고 자동화가 가능하다는 걸 알게 됐다. 원체 다양한 벤더와 라이브러리를 자랑하는 언어다 보니 상황에 따라 해결책이 달라지긴 하지만 이 칼럼을 통해 “해결할 수 있다”라는 자신감을 얻을 수 있길 바란다.

최 재훈

블로그, 페이스북, 트위터 고성능 서버 엔진, 데이터베이스, 지속적인 통합 등 다양한 주제에 관심이 많다.
Close Menu