S/W 개발 기반 시스템 구성 – 8. 빌드 스크립트 구성 v2

이 글은 S/W 개발에 가장 기본이 되는 이슈 추적(Issue Tracker), 버전 관리(Version Control), 빌드(Build), 지속적인 통합(CI) 시스템을 구성하는 방법에 대한 일련의 글 중 여덟 번째이다. 빌드 스크립트에 대한 내용은 지난 글에서 이미 다뤘으므로 상황에 따라 응용하는 것으로 충분하지만 이 글에서는 다음 몇 가지 내용을 개선한 완성판이랄 수 있어 정리해 둔다.

1. 개선 내용

  • 표준 디렉터리 구조 사용
  • Visual Studio 6과 Visual Studio 2010 동시 지원
  • 빌드 스크립트 호출 방식 간소화

2. 미리 준비해야 하는 것들

지난 글에서와 마찬가지로 NAnt, Visual Studio 6, Visual Studio 2010을 사용한다. Visual Studio 2010에서는 MSBuild를 사용할 수도 있지만 이 글에서는 devenv.com과 솔루션 파일을 사용해 빌드한다.

3. 빌드 스크립트

프로젝트 구조는 다음과 같다.

Standard Project Structure

  • bin: 빌드 후 실행 파일(exe, dll)을 모은다. 프로젝트 설정 중 post-build event에서 copy 명령으로 처리한다.
  • bintools: 프로젝트에서 사용하는 툴 프로젝트에서 만든 실행 파일을 모은다. 마찬가지로 post-build event에서 copy 명령으로 처리한다.
  • build: 빌드 스크립트를 둔다.
  • srcapp: core 프로젝트를 기반으로 개발하는 응용 프로그램 프로젝트를 관리한다.
  • srccore: 기반이 되는 라이브러리 프로젝트를 관리한다.
  • srclib: core 프로젝트에서 만든 라이브러리(lib)를 다른 프로젝트에서 참조할 수 있도록 모은다. 마찬가지로 post-build event에서 copy 명령으로 처리한다.
  • srctools: 핵심 프로그램은 아니지만 개발 작업을 편하게 하는 등 유틸리티 프로그램을 관리한다.
  • vendor: 서드 파티 라이브러리를 둔다. 각 라이브러리별로 include, lib 디렉터리를 아래에 둬 구조를 일관되게 유지한다.
  • 테스트 결과 파일은 bin과 bintools에 실행 파일과 함께 저장하거나 별도 디렉터리를 만들어 저장한다.

build 디렉터리에 있는 빌드 스크립트는 다음과 같다.

Build Script Files

NAnt에서는 .build로 끝나는 파일을 빌드 스크립트로 자동 인식하므로 스크립트 파일을 따로 지정하지 않아도 돼 빌드 명령도 간편해진다.

- Release build
 : build

- Debug build
 : build -D:build=Debug

- Clean
 : build clean

- Clean & Release Build
 : build clean build

- Clean & Debug Build
 : build clean build -D:build=Debug

- Help
 : build /?

* Clean 빌드는 IDE에서 사용하는 임시 파일도 삭제하므로 IDE 종료 후 실행 할 것

배치 파일 역시 좀 더 간편하게 바꿨다. 이전과 달리 배치 파일만 실행하면 release 빌드를 실행한다.

@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  : build
echo.
echo - Debug build
echo  : build -D:build=Debug
echo.
echo - Clean
echo  : build clean
echo.
echo - Clean ^& Release Build
echo  : build clean build
echo.
echo - Clean ^& Debug Build
echo  : build clean build -D:build=Debug
echo.
echo - Help
echo  : build /?
echo.
echo * Clean 빌드는 IDE에서 사용하는 임시 파일도 삭제하므로 IDE 종료 후 실행 할 것
echo.

:QUIT

스크립트 내용은 다음과 같다. 기본 내용은 크게 바뀌지 않았으나 세부적인 부분에서 VC++ 6과 VC++ 10을 함께 쓰는 것을 고려했다.

<?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="vc6_release" value="Win32 Release" />
  <property name="vc6_debug" value="Win32 Debug" />

  <if test="${build!='Release' and build!='Debug'}">
    <fail message="The build configuration doesn't exist! So exit the process." />
  </if>

  <!-- 빌드 설정 : VC6 -->
  <if test="${build=='Release'}">
    <property name="vc6_build" value="${vc6_release}" />
  </if>
  <if test="${build=='Debug'}">
    <property name="vc6_build" value="${vc6_debug}" />
  </if>

  <!-- 빌더 환경 변수 : VC6 -->
  <if test="${environment::variable-exists('MSDevDir')}">
    <property name="vc6_path" value="${environment::get-variable('MSDevDir')}" />
    <echo message="${vc6_path}" />
  </if>

  <!-- 빌더 환경 변수 : VC10 -->
  <if test="${environment::variable-exists('VS100COMNTOOLS')}">
    <property name="vcnet_path" value="${environment::get-variable('VS100COMNTOOLS')}" />
    <echo message="${vcnet_path}" />
  </if>

  <if test="${not property::exists('vc6_path') and not property::exists('vcnet_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>
        <exclude name="**/vendor/**" />
        <include name="bin/**" />
        <include name="src/lib/**" />
        <include name="**/*.aps" />
        <include name="**/*.ncb" />
        <include name="**/*.opt" />
        <include name="**/*.plg" />
        <include name="**/*.suo" />
        <include name="**/*.user" />
        <include name="**/*.sdf" />
        <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">
    <mkdir dir="bin" />
    <mkdir dir="bin/tools" />
    <mkdir dir="src/lib" />
    <call target="make_file_version" />
    <if test="${server_build!='no'}">
      <call target="make_product_version" />
    </if>
    <call target="build_core" />
    <call target="build_app" />
    <call target="build_tool" />
    <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">
    <!-- build : VC6 -->
    <foreach item="File" property="filename">
      <in>
        <items>
          <include name="**/core/**/*.dsp" />
        </items>
      </in>
      <do>
        <regex pattern="^(?'path'.*(\\|/)|(/|\\))(?'name'.*)\.(?'extension'\w+)$" input="${filename}" />
        <exec program="${vc6_path}\Bin\msdev.com">
          <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 -->
          <arg line='"${filename}" /MAKE "${name} - ${vc6_build}" /REBUILD' />
        </exec>
      </do>
    </foreach>
    <!-- build : VC10 -->
    <foreach item="File" property="filename">
      <in>
        <items>
          <include name="**/core/**/*.sln" />
        </items>
      </in>
      <do>
        <exec program="${vcnet_path}\..\ide\devenv.com">
          <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 -->
          <arg line='"${filename}" /rebuild "${build}"' />
        </exec>
      </do>
    </foreach>
  </target>

  <!-- build : application programs -->
  <target name="build_app" description="compiles the application programs">
    <!-- build : VC6 -->
    <foreach item="File" property="filename">
      <in>
        <items>
          <include name="**/app/**/*.dsp" />
        </items>
      </in>
      <do>
        <regex pattern="^(?'path'.*(\\|/)|(/|\\))(?'name'.*)\.(?'extension'\w+)$" input="${filename}" />
        <exec program="${vc6_path}\Bin\msdev.com">
          <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 -->
          <arg line='"${filename}" /MAKE "${name} - ${vc6_build}" /REBUILD' />
        </exec>
      </do>
    </foreach>
    <!-- build : VC10 -->
    <foreach item="File" property="filename">
      <in>
        <items>
          <include name="**/app/**/*.sln" />
        </items>
      </in>
      <do>
        <exec program="${vcnet_path}\..\ide\devenv.com">
          <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 -->
          <arg line='"${filename}" /rebuild "${build}"' />
        </exec>
      </do>
    </foreach>
  </target>

  <!-- build : tool programs -->
  <target name="build_tool" description="compiles the tool programs">
    <!-- build : VC6 -->
    <foreach item="File" property="filename">
      <in>
        <items>
          <include name="**/tools/**/*.dsp" />
        </items>
      </in>
      <do>
        <regex pattern="^(?'path'.*(\\|/)|(/|\\))(?'name'.*)\.(?'extension'\w+)$" input="${filename}" />
        <exec program="${vc6_path}\Bin\msdev.com">
          <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 -->
          <arg line='"${filename}" /MAKE "${name} - ${vc6_build}" /REBUILD' />
        </exec>
      </do>
    </foreach>
    <!-- build : VC10 -->
    <foreach item="File" property="filename">
      <in>
        <items>
          <include name="**/tools/**/*.sln" />
        </items>
      </in>
      <do>
        <exec program="${vcnet_path}\..\ide\devenv.com">
          <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 -->
          <arg line='"${filename}" /rebuild "${build}"' />
        </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>

4. 빌드 순서 지정하기

프로젝트 의존성에 따라 빌드 순서를 지정해야 할 수도 있다. VC++ 10에서는 솔루션 파일을 사용하므로 해당 솔루션에 속하는 프로젝트에 대해 빌드 순서를 지정해 둘 수 있다. 하지만 VC++ 6에서는 프로젝트 파일을 사용하므로 의존성을 알 수 없고 VC++ 10인 경우라도 솔수션 사이에 빌드 순서를 지정해야 한다면 곤란한 상황이 된다.

NAnt에서는 task 속성 중 <target>을 사용해 의존성을 지정할 수 있지만 그런 경우 프로젝트별로 <target>을 사용해야 하는 단점이 있다. 즉 의존성이 생길 때마다 <target>과 그 아래에 딸린 내용을 일일이 추가하는 것은 상당히 불편하다는 말씀. 게다가 여기서는 <target>을 각 프로젝트별로 하나씩 사용하는 것이 아니라 프로젝트 구조에 맞춰 사용하고 해당 디렉터리 안에 있는 프로젝트는 몽땅 묶어 빌드하는데 이는 프로젝트를 추가하더라도 스크립트를 고치지 않아도 되는 장점이 있다. 그러므로 이런 장점을 유지하면서도 빌드 순서를 지정할 수 있는 간편한 방법이 필요하다.

이럴 때는 다음과 같이 한다.

    <foreach item="File" property="filename">
      <in>
        <items>
          <include name="**/core/**/first.dsp" />
          <include name="**/core/**/*.dsp" />
        </items>
      </in>
      ...
    </foreach>

먼저 빌드해야 하는 프로젝트 또는 솔루션 이름을 에서 앞에 지정한다. 위 예에서는 first.dsp 파일을 사용해 해당 프로젝트를 먼저 빌드한 후 first.dsp를 제외한 나머지 프로젝트 파일을 빌드한다.

5. CI 빌드 설정

스크립트를 바꿨으므로 그에 맞춰 CI 빌드 설정도 약간 바뀐다.

Jenkins Build of the Job

.buildbuild.bat /f:.builddefault.build clean build -D:server_build=Daily -D:product_major=1 -D:product_minor=1 -D:product_patch=1

테스트 프레임워크로 google test를 사용한다면 JUnit과 출력 구조가 같으므로 선택 후 결과 파일만 지정하면 별도 플러그인 없이 바로 결과 내용을 볼 수 있다.

Jenkins Post-build Actions of the Job

You may also like...