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:nantbin을 환경 변수에 추가한다.

소스 컴파일을 위해 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. 빌드 스크립트

먼저 예로 든 프로젝트 구조는 다음과 같다.

Project Structure

Build 디렉터리에는 빌드 스크립트 파일이 있다.

Build Script Files

‘Build.txt’ 파일 내용은 다음과 같다.

– 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 파일은 다음과 같다(추가: 배치 파일만 실행하면 도움말 출력).

@echo off
if "%1" == "" goto HELP
SET PATHSAVED=%PATH%
call "%MSDevDir%....VC98BinVCVARS32.BAT"
"C:nantbinNAnt.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 스크립트 파일이며 내용은 다음과 같다.

<?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 일 때는 해당 값을 사용해 태그를 만들어 구분할 수 있다. 또한 이 값을 사용해 빌드 종류에 따라 처리할 수도 있다.


[1] Windows Server 2008 R2에 Visual Studio 6을 설치할 때 주의할 사항은 다음과 같다. MS Java VM을 설치할 때 호환되지 않는다고 나오면 Run program으로 계속 진행한다. 설치 내용 중 Enterprise Tools에서 Visual Studio Analyzer, Tools에서 OLE/Com Object Viewer 정도가 문제를 일으킬 수 있는데 빌드만 하면 되니 Enterprise Tools, Tools 전체를 아예 빼 버리는 게 좋다.
그래도 설치에 문제가 있으면 다음과 같이 설치해 보자. 설치 파일이 E:VS6Ent에 있다고 가정한다.
E:VS6EntSETUPACMSETUP.EXE /T VS98ENT.STF /S E:VS6Ent /n "사용자이름" /o "회사이름" /k "YYYYYYYYYY" /b 1

YYYYYYYYYY는 시리얼 번호(열 자리)이고 VS98ENT.STF는 SETUP 디렉터리에 있는 STF 파일이름이다.

You may also like...

  • Albert

    ‘파일 버전은 개발자가 리소스 정보 파일에서 major, minor, patch 버전까지 지정한다.’는 의미가 무엇인가요?

    *.rc2에 #define major 1 과 같은 형식의 정의를 의미하는 것인가요?

    • rc2 파일에서 파일 버전을 나타내는 항목은 다음입니다.
      FILEVERSION 1,0,0,0
      VALUE “FileVersion”, “1.0.0.0”
      rc2 파일을 열고 1.0.0.0에서 앞의 세 숫자만 직접 바꾼다는 의미입니다.