S/W 개발 기반 시스템 구성 – 5. 빌드 스크립트 구성
이 글은 S/W 개발에 가장 기본이 되는 이슈 추적(Issue Tracker), 버전 관리(Version Control), 빌드(Build), 지속적인 통합(CI) 시스템을 구성하는 방법에 대한 일련의 글 중 다섯 번째이다. 지속적인 통합을 위해 가장 필요한 것 중 하나가 빌드를 쉽게 하는 것이다. 빌드 과정이 복잡하면 실수는 물론이고 자주 빌드하며 통합하기도 어렵다. 그러므로 빌드를 자동화하는 것은 매우 중요하다. 이번에는 빌드 스크립트를 구성해 개발자는 물론이고 CI에서도 함께 사용할 수 있도록 해 본다.
1. 미리 준비해야 하는 것들
- 빌드에는 NAnt를 사용한다. 주개발 언어가 자바라면 선택지가 많겠지만 윈도에서, 그것도 C++를 주언어로 하는 터라 선택의 여지가 그리 없다. NAnt는 여기에서 바이너리 파일을 받으면 된다.
- Visual Studio 6
- Visual Studio 2010
2. 설치
NAnt는 압축 파일 내용을 풀어 주면 설치는 끝이다. 호출할 때 사용할 경로를 모두 일치시키기 위해 C:\nant
로 복사한다. 명령 행에서도 호출하려면 C:\nant\bin
을 환경 변수에 추가한다.
소스 컴파일을 위해 Visual Studio 6과 Visual Studio 2010을 사용하므로 서버에 설치해 둔다[1]. 설치 순서는 VS6 – VS6 SP6 – VS2010 – VS2010 SP1이다. Visual Studio 6은 반드시 설치해야 한다. Visual Studio 2010 C++ 프로젝트는 MSBuild로 직접 빌드하거나 NAnt에서 MSBuild를 사용해 빌드할 수 있다. 또는 Visual Studio 6에서 msdev.com
을 사용해 컴파일하는 것처럼 devenv.com
을 사용할 수도 있다. MSBuild를 사용하면 Visual Studio 2010을 설치하지 않아도 되지만 devenv.com
파일을 사용하면 설치해야 한다.
3. 빌드 스크립트
먼저 예로 든 프로젝트 구조는 다음과 같다.
Build 디렉터리에는 빌드 스크립트 파일이 있다.
Build.txt
파일 내용은 다음과 같다.
12345678910111213141516 - Release build: vc6_build /f:vc6.build.xml- Debug build: vc6_build /f:vc6.build.xml -D:build=debug- Clean: vc6_build /f:vc6.build.xml clean- Clean & Release Build: vc6_build /f:vc6.build.xml clean build- Clean & Debug Build: vc6_build /f:vc6.build.xml clean build -D:build=debug* Clean 빌드는 IDE에서 사용하는 임시 파일도 삭제하므로 IDE 종료 후 실행 할 것
vc6_build.bat
파일은 다음과 같다(추가: 배치 파일만 실행하면 도움말 출력).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@echo off if "%1" == "" goto HELP SET PATHSAVED=%PATH% call "%MSDevDir%\..\..\VC98\Bin\VCVARS32.BAT" "C:\nant\bin\NAnt.exe" %* SET ERR_LEVEL=%errorlevel% SET PATH=%PATHSAVED% SET PATHSAVED= exit /b %ERR_LEVEL% :HELP echo. echo - Release build echo : vc6_build /f:vc6.build.xml echo. echo - Debug build echo : vc6_build /f:vc6.build.xml -D:build=debug echo. echo - Clean echo : vc6_build /f:vc6.build.xml clean echo. echo - Clean ^& Release Build echo : vc6_build /f:vc6.build.xml clean build echo. echo - Clean ^& Debug Build echo : vc6_build /f:vc6.build.xml clean build -D:build=debug echo. echo * Clean 빌드는 IDE에서 사용하는 임시 파일도 삭제하므로 IDE 종료 후 실행 할 것 echo. :QUIT |
vc6.build.xml 파일은 NAnt 스크립트 파일이며 내용은 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
<?xml version="1.0" encoding='UTF-8'?> <project name="SandBox" default="build" basedir=".."> <!-- 서버 빌드인지 확인 (서버 빌드할 때 지정): ci, Daily, Release --> <property name="server_build" value="no" unless="${property::exists('server_build')}" /> <!-- 제품 버전 : 빌드와 태그 만들 때 사용 (서버 빌드할 때 지정) --> <property name="product_major" value="0" unless="${property::exists('product_major')}" /> <property name="product_minor" value="0" unless="${property::exists('product_minor')}" /> <property name="product_patch" value="0" unless="${property::exists('product_patch')}" /> <if test="${server_build!='no'}"> <!-- Jenkins environment variable : build number --> <property name="product_build" value="${environment::get-variable('BUILD_NUMBER')}" /> <echo message="Build Product Ver. No: ${product_major}.${product_minor}.${product_patch}.${product_build}" /> </if> <!-- 빌드 설정 (서버 또는 개발자 빌드할 때 지정)--> <property name="build" value="release" unless="${property::exists('build')}" /> <property name="build_release" value="Win32 Release" /> <property name="build_debug" value="Win32 Debug" /> <!-- 빌더 환경 변수 --> <if test="${environment::variable-exists('MSDevDir')}"> <property name="builder_path" value="${environment::get-variable('MSDevDir')}" /> <echo message="${builder_path}" /> </if> <if test="${not property::exists('builder_path')}" > <fail message="Cann't find builder! So exit the process." /> </if> <!-- clean --> <target name="clean" description="remove all generated files"> <delete verbose="true"> <fileset> <include name="**/*.aps" /> <include name="**/*.ncb" /> <include name="**/*.opt" /> <include name="**/*.plg" /> <include name="**/Release/**" /> <include name="**/Debug/**" /> <include name="**/Release-Temp/**" /> <include name="**/Debug-Temp/**" /> </fileset> </delete> </target> <!-- make file version information --> <target name="make_file_version" description="make file build information"> <foreach item="File" property="filename"> <in> <items> <include name="**/*.rc2" /> </items> </in> <do> <loadfile file="${filename}" property="fileContent" verbose="false" /> <regex pattern="(?'major'\d+)*,(?'minor'\d+)*,(?'patch'\d+)*,(?'oldBuild'\d+)" input="${fileContent}" /> <property name="newBuild" value="${int::parse(oldBuild)+1}" /> <copy file="${filename}" tofile="${filename}.tmp" overwrite="true" > <filterchain> <replacestring from="${major},${minor},${patch},${oldBuild}" to="${major},${minor},${patch},${newBuild}" ignorecase="true" /> <replacestring from="${major}, ${minor}, ${patch}, ${oldBuild}" to="${major}, ${minor}, ${patch}, ${newBuild}" ignorecase="true" /> </filterchain> </copy> <delete file="${filename}" /> <move file="${filename}.tmp" tofile="${filename}" /> </do> </foreach> </target> <!-- make product version information --> <target name="make_product_version" description="make product build information"> <foreach item="File" property="filename"> <in> <items> <include name="**/product_version.h" /> </items> </in> <do> <loadfile file="${filename}" property="fileContent" verbose="false" /> <regex pattern="(?'major'\d+)*,(?'minor'\d+)*,(?'patch'\d+)*,(?'oldBuild'\d+)" input="${fileContent}" /> <copy file="${filename}" tofile="${filename}.tmp" overwrite="true" > <filterchain> <replacestring from="${major},${minor},${patch},${oldBuild}" to="${product_major},${product_minor},${product_patch},${product_build}" ignorecase="true" /> <replacestring from="${major}, ${minor}, ${patch}, ${oldBuild}" to="${product_major}, ${product_minor}, ${product_patch}, ${product_build}" ignorecase="true" /> </filterchain> </copy> <delete file="${filename}" /> <move file="${filename}.tmp" tofile="${filename}" /> </do> </foreach> </target> <!-- build --> <target name="build" description="compiles the source code"> <call target="make_file_version" /> <if test="${server_build!='no'}"> <call target="make_product_version" /> </if> <call target="build_core" /> <call target="build_app" /> <if test="${server_build=='Daily' or server_build=='Release'}"> <call target="tagging" /> </if> </target> <!-- build : core libraries --> <target name="build_core" description="compiles the core libraries"> <if test="${build!='debug'}"> <property name="configuration" value="${build_release}" /> </if> <if test="${build=='debug'}"> <property name="configuration" value="${build_debug}" /> </if> <foreach item="File" property="filename"> <in> <items> <include name="**/core/**/*.dsp" /> </items> </in> <do> <regex pattern="^(?'path'.*(\\|/)|(/|\\))(?'name'.*)\.(?'extension'\w+)$" input="${filename}" /> <exec program="${builder_path}\Bin\msdev.com"> <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 --> <arg line='"${filename}" /MAKE "${name} - ${configuration}" /REBUILD' /> </exec> </do> </foreach> </target> <!-- build : application programs --> <target name="build_app" description="compiles the application programs"> <if test="${build!='debug'}"> <property name="configuration" value="${build_release}" /> </if> <if test="${build=='debug'}"> <property name="configuration" value="${build_debug}" /> </if> <foreach item="File" property="filename"> <in> <items> <include name="**/app/**/*.dsp" /> </items> </in> <do> <regex pattern="^(?'path'.*(\\|/)|(/|\\))(?'name'.*)\.(?'extension'\w+)$" input="${filename}" /> <exec program="${builder_path}\Bin\msdev.com"> <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 --> <arg line='"${filename}" /MAKE "${name} - ${configuration}" /REBUILD' /> </exec> </do> </foreach> </target> <!-- tagging --> <target name="tagging" description="commit modified files, and tagging"> <!-- commmit modified files (version information) --> <exec program="C:\Python26\Scripts\hg.bat"> <arg line='commit -m "${server_build} build(Build No: ${product_build}). Added modified version information files"'/> </exec> <!-- get changeset --> <exec program="C:\Python26\Scripts\hg.bat" output="changeset.txt"> <arg line="id -i" /> </exec> <loadfile file="changeset.txt" property="changeset" /> <delete file="changeset.txt" /> <!-- tagging --> <exec program="C:\Python26\Scripts\hg.bat"> <arg line='tag -m "${server_build} build(Build No: ${product_build}). Added tag ${product_major}.${product_minor}.${product_patch}.${product_build} for changeset ${string::trim(changeset)}" ${product_major}.${product_minor}.${product_patch}.${product_build}'/> </exec> <!-- push --> <exec program="C:\Python26\Scripts\hg.bat"> <arg line="push"/> </exec> </target> </project> |
실행 파일 버전 정보를 보면 제품 버전과 파일 버전이 있다. 버전 정보는 major, minor, patch, build 형식을 사용한다. 파일 버전은 개발자가 리소스 정보 파일에서 major, minor, patch 버전까지 지정한다. 마지막 build 버전은 개발자가 빌드할 때 스크립트에서 자동으로 증가시킨다. 제품 버전은 곧 패키지 버전이며 서버에서 빌드할 때 major, minor, patch 버전까지 지정한다. 마지막 build 버전은 파일 버전과 마찬가지로 스크립트에서 자동으로 증가시킨다. 서버에서는 빌드 후 Mercurial 저장소에 태깅까지 한다.
프로젝트 구조에서 기반으로 사용하는 코어 라이브러리와 실행 프로그램을 나눴기 때문에 빌드 역시 그에 맞춰 따로 빌드할 수 있도록 했다. 실제 적용은 적절히 사용하면 되겠다.
개발자는 Build.txt
파일 내용에 있는 대로 빌드하면 된다.
추가: 서버 빌드를 지정할 수 있도록 했다. server_build 값이 ci일 때는 태그를 만들지 않으며 Daily, Release 일 때는 해당 값을 사용해 태그를 만들어 구분할 수 있다. 또한 이 값을 사용해 빌드 종류에 따라 처리할 수도 있다.
그래도 설치에 문제가 있으면 다음과 같이 설치해 보자. 설치 파일이 E:VS6Ent에 있다고 가정한다.
1 |
E:\VS6Ent\SETUP\ACMSETUP.EXE /T VS98ENT.STF /S E:\VS6Ent /n "사용자이름" /o "회사이름" /k "YYYYYYYYYY" /b 1 |
YYYYYYYYYY는 시리얼 번호(열 자리)이고 VS98ENT.STF는 SETUP 디렉터리에 있는 STF 파일이름이다.