AMBuild


아마 여기서 이 글을 보는 사람이라면 AMBuild는 소스모드의 익스텐션만을 빌드하는 툴로만 알고 있을텐데 사실은 C++ 프로젝트라면 어디에나 호환될 수 있도록 alliedmods 측에서 제작한 빌드 툴이다. 익스텐션에 대한 내용은 나중에 다루고 우선 그 빌드에 쓰이는 이 도구에 대해 알아보고자 한다.

먼저 이 빌드 툴이 다른 빌드 툴과 무슨 차이가 있는지 알아보기 위해 위키를 참고했다. 여기서 대부분의 빌드 툴에서 겪는 3가지의 큰 문제점을 해결하였다고 설명하고 있는데:

  • 정확도 : 빌드 과정을 새로 만들어 시간을 낭비하는 일이 없도록 AMBuild에서 항상 최소한의 빌드할 것들을 정확하게 짚어낸다.
  • 속도 : 대부분의 빌드 툴에서는 변경 사항을 찾아내기 위해 모든 의존을 순회하지만 AMBuild에서는 실제로 바뀐 파일만을 확인한다.
  • 유연성 : 빌드에 사용되는 스크립트는 Python으로 작성하여 프로그래밍 방식으로 쉽게 작성하고 제공할 수 있다.

또한 왜 이런 빌드 툴을 제작하였는지에 대한 배경도 흥미롭다.

소스모드나 익스텐션을 빌드하는 경우, 보통 각 사용할 게임마다 SDK의 링크를 다르게 설정해야 하고 컴파일러에 넣을 플래그나 순서 등의 커스터마이징도 그에 맞춰야 하는데 여기에서 생기는 빌드 시간 증가나 의존성 확인 등의 문제를 해결해야 했다는 것이다. 그래서 만들어진 지금 버전의 AMBuild은 현대 빌드 시스템인 Tup을 모델로 하였으며 언젠가는 Tup을 백엔드로 활용할 예정이라고 한다.

물론 이 툴은 널리 쓰이지도 않았고 다양한 종류의 시스템에서 사용된 것은 아니지만 소스모드에서 사용된 사례를 보고 흥미를 느껴 확인해본다.

요구사항

Python 버전 3.3 이상이 필요하고 추가적으로 파이썬 컴포넌트인 pip, setuptool도 필요하다.

아래 플랫폼에서는 정상적으로 작동되는 것이 확인된 상태다.

  • Windows 7+ with x86, x86_64, arm, or arm64 targets.
  • Linux with clang and GCC.
  • OS X 10.9+ with x86 and x86_64 targets.

컴파일러의 경우는 아래와 같다.

  • Visual Studio 2015+
  • GCC 4+
  • Clang 3+
  • Emscripten 1.25+ (JS output mode only, lib deps do not work)
기술 개요

AMBuild는 프론트엔드와 백엔드로 나뉜다. 프론트엔드에서는 빌드 스크립트 분석을 담당하고 (구성(configure) 단계), 백엔드에서는 실제 빌드를 담당한다. 프론트와 백엔드가 나뉘어져 있으니 AMBuild 외의 다른 백엔드를 사용할 수도 있는데 예를 들면 구성 단계에서 Visual Studio 프로젝트 파일을 생성하게 할 수도 있다.

구성 단계

빌드를 구성하는 스크립트는 Python으로 작성된다. 이 스크립트가 변경되면 다음 빌드에 자동적으로 구성 단계를 다시 시작하고 새로운 의존성을 확인하면 이전 빌드에서 남겨진 모든 파일을 제거한다.

생성된 의존성 그래프의 세부 사항은 빌드 경로의 숨겨진 .ambuild2 폴더에 SQLite 데이터베이스로 저장된다.

빌드

빌드에 항상 사용되는 ambuild 명령어에는 몇 가지 중요한 단계가 있다.

1. Damage 계산: 파일 시스템의 타임 스탬프를 기준으로 지난 빌드 이후에 바뀐 파일의 목록을 생성한다. 시간이 일치하지 않는다면 “damaged” (또는 Dirty)되었다고 고려한다.

$ ambuild --show-changed
/home/dvander/alliedmodders/mmsource-central/loader/loader.cpp

2. 부분적인 의존성 그래프 생성: damaged 된 파일들을 기반으로 부분적인 의존성 그래프를 생성한다. 아래 예제는 그래프의 요소 중에 다시 계산해야 하는 것을 보여준다.

$ ambuild --show-damage
 - package/addons/metamod/bin/server_i486.so
  - cp "../loader/server_i486/server_i486.so" "package/addons/metamod/bin/server_i486.so"
   - loader/server_i486/server_i486.so
    - c++ loader.o gamedll.o serverplugin.o utility.o -m32 -static-libgcc -shared -o server_i486.so
     - loader/server_i486/loader.o
      - [gcc] -> c++ -Wall -Werror -H -c /home/dvander/alliedmodders/mmsource-central/loader/loader.cpp -o loader.o
       - /home/dvander/alliedmodders/mmsource-central/loader/loader.cpp

3. 작업 생성: 부분적인 의존성 그래프는 명령어들의 트리로 단순해진다. 이 단계의 출력은 –show-commands로 보일 수 있다.

$ ambuild --show-commands
 - cp "../loader/server_i486/server_i486.so" "package/addons/metamod/bin/server_i486.so"
  - c++ loader.o gamedll.o serverplugin.o utility.o -shared -o server_i486.so
   - [gcc] -> c++ -Wall -Werror -H -c /home/dvander/alliedmodders/mmsource-central/loader/loader.cpp -o loader.o

4. 업데이트: 생성된 명령어 트리의 각 작업 결과로 빌드가 거절되거나 의존성 그래프의 상태를 업데이트한다. 이 단계에서는 CPU 코어의 가용 가능한 갯수에 따라 병렬로 실행되며 처리량을 극대화하기 위해 그래프도 공유된다. 잇다라 실행되는 빌드 과정은 –show-steps로 확인할 수 있다.

$ ambuild --show-steps
task 0: [gcc] -> c++ -Wall -Werror -H -c /home/dvander/alliedmodders/mmsource-central/loader/loader.cpp -o loader.o
  -> loader/server/loader.o
task 1: c++ loader.o gamedll.o serverplugin.o utility.o -m32 -static-libgcc -shared -o server.so
  -> loader/server/server.so
task 2: cp "../loader/server/server.so" "package/addons/metamod/bin/server.so"
  -> package/addons/metamod/bin/server.so

AMBuild의 작업 프로세스는 작업을 수행하여 결과를 반환하고 CPU 가용 코어 갯수에 따라 생성된다.
파이썬에서의 처리량 극대화는 IPC와 멀티스레딩의 한정된 용량으로 인해 까다롭게 구현되었다.

비교

Make, 속도

Make는 상위-레벨 규칙에 따라 재귀적으로 모든 의존성을 확인하고 뒤쳐진 규칙들을 찾아내서 모든 규칙들을 업데이트한다. 이는 보통 필요한 양보다 더 많이 건드리게 된다. 아래 그래프를 참고해보자.

이 그래프에서 stdint.h가 변경되었는데 이 하나의 요소만 바뀌었음에도 Make는 그래프 내의 모든 요소를 뒤져서 변경 사항을 찾아내야 한다. 여기서 main.o나 helpers.o로 인해 이걸 몇 번 더 하므로 만약 큰 의존성 그래프에서 수행했다면 상당히 무거워질 수 있다.
AMBuild에서는 그래프의 방향이 반대로 된다.

이 그래프에서는 모든 노드를 방문하는 횟수를 한번으로 줄이는 것이 가능하다. stdint.h가 바뀐 것을 알아냈을 때, 해당 파일을 기준으로 위로 이동하고 영향을 받지 않는 모든 것들을 무시하기 때문이다.

Make, 정확도

AMBuild는 정확도를 위해 2가지 방식을 설계했다.

  • 정확도를 유지하는 선에서 의존성 연산이 이뤄지도록 함
    빌드 스크립트를 바꾸는 것이 완전 새롭게 빌드하지 않도록 함
  • 차근차근 쌓여가는 빌드가 새로 만든 빌드와 정확히 같도록 함

Make에서라면 차근히 쌓는 빌드를 아주 정확하게 생성하는 것이 쉽지가 않다. 예를 들어 소스 파일과 CFLAGS의 목록인 Makefile으로 C++ 컴파일러에 빌드하고 링크한다고 생각해보자.

# Makefile
CFLAGS = -Wall

helpers.o: helpers.cpp
  $(CC) $(CFLAGS) helpers.cpp -c -o helpers.o
main.o: main.cpp
  $(CC) $(CFLAGS) main.cpp -c -o main.o

main: main.o helpers.o
  $(CC) main.o helpers.o -o main

여기서 helpers.cpp를 수정했다면 최소의 빌드가 생성된다. 새로운 소스 파일을 추가했어도 비슷하게 최소의 재빌드가 생성될테지만 만약 helpers 규칙을 아예 삭제했다면 Make가 main이 다시 링크해야 한다는 것을 알리지 못해 빌드 자체가 맞지 않게 된다.

이걸 해결하기 위해서는 Makefile 자체에 의존을 갖게 하는 연결 규칙 같은 특정한 규칙을 가지게 하면 되지만 만약 CFLAGS를 바꿀 때는 규칙에서 CFLAGS가 의존되지 않아도 다시 링크하게 되므로 아주 많고 작은 Makefile으로 쪼개서 피해야 할 수 있다.

AMBuild는 이런 문제에 의존성 그래프를 지능적으로 업데이트함으로써 완화한다. 빌드 스크립트를 새로 분석한 결과로 동일한 요소를 만들었다면 이 요소들을 업데이트 할 필요가 없는 것으로 고려한다.

나아가서 Make에서 규칙을 삭제하면 그 출력을 남기긴 하지만 이걸 토대로 문제를 해결하기엔 출력의 내용이 부실하여 실패했던 빌드도 성공했다고 유저나 개발자를 속일 수도 있고 출력이 실수로 로드되는 경우(LD_LIBRARY_PATH에서 존재하지 않은 라이브러리를 골라버린 경우)도 있어 쉽지 않다. 테스트가 로컬에서는 통과되었겠지만 알고보니 개발자가 업데이트하는 것을 잊어버렸던 오래된 출력 때문에 그랬을 수도 있다는 것이다.

이에 AMBuild는 새로운 빌드에선 파일을 남기지 않도록 하여 그런 오래된 출력을 남긴 걸 오류로 보고 있다.

튜토리얼
단순한 프로젝트 만들기

아래와 같은 파일들을 가진 예제 프로젝트가 있다.

$ ls
goodbye.cpp  helpers.cpp  README.txt

시작하기 위해서는 기본 AMBuild 구성 스크립트를 생성해야 한다. 구성 스크립트는 앞서 말한 ‘구성 단계’를 수행한다. 아래 명령어를 사용해 생성할 수 있다.

$ ambuild --new-project
$ ls
AMBuildScript  configure.py  goodbye.cpp  helpers.cpp  README.txt

구성 스크립트(configure.py)는 그저 AMBuild를 호출하지만 추가적인 명령어 매개변수를 받기 위해 수정할 수 있다. (나중에 또 얘기한다.)

$ cat configure.py
# vim: set sts=2 ts=8 sw=2 tw=99 noet:
import sys
from ambuild2 import run

prep = run.BuildParser(sys.path[0], api='2.2')
prep.Configure()

이제 프로젝트에 빌드 스크립트를 만들자. 마스터 빌드 스크립트인 AMBuildScript는 Python으로 작성되어야 하고 여기에 모든 파이썬 API를 사용할 수 있다. 물론 여기서 주로 중요한 AMBuild의 API를 다룰거지만 말이다.
첫 번째로 아래에 있는 한 줄의 코드만 넣어보자. 그러면 빌드를 구성할 수 있게 된다.

cxx = builder.DetectCxx()

이 한 줄의 빌드 스크립트가 있다면 빌드를 구성해볼 수 있게 된다.

$ mkdir build
$ cd build
$ python ../configure.py
Checking CC compiler (vendor test gcc)... ['cc', 'test.c', '-o', 'test']
found gcc version 4.7
Checking CXX compiler (vendor test gcc)... ['c++', 'test.cpp', '-o', 'testp']
found gcc version 4.7
$

에러가 나왔다면 컴파일러가 설치되어 있지 않았을 수도 있다. gcc, clang, Microsoft Visual Studio가 설치되었는지 확인해보자.
이제 AMBuildScript를 마저 작성해보자.

program = cxx.Program("hello")
program.sources = [
  'main.cpp',
  'helpers.cpp',
]
builder.Add(program)

builder 오브젝트는 AMBuild의 인스턴스를 의미한다. (이건 AMBuild API 문서에서 더 다룬다.) 모든 AMBuild 스크립트는 이 builder에 접근해야 한다. builder.cxx 오브젝트는 구성 세션에서 C/C++ 컴파일러의 정보를 가진다. Program() 메소드는 C++ 컴파일 작업을 생성하는 오브젝트를 반환한다. 따라서 이 경우에는 hello라는 이름(Windows의 경우 ‘hello’)의 실행 파일을 빌드한다고 생각하면 된다. 또한 공유 라이브러리(Library)나 정적 라이브러리(StaticLibrary)를 지정할 수 있다.
소스코드 목록을 프로그램에 넣기 위해선 sources 속성에 넣고 그런 다음 마지막으로 builder.Add로 C++ 구성을 가져가 필요한 의존성 그래프와 빌드 단계를 구성하도록 한다.
그러면 이제 빌드를 할 수 있게 되는데 그 전에 먼저 AMBuild가 그래프 계산을 제대로 하였는지 확인하자.

$ python ../configure.py
$ ambuild --show-graph
 : mkdir "hello"
 - hello/hello
   - c++ main.o helpers.o -o hello
     - hello/main.o
       - [gcc] -> c++ -H -c /home/dvander/projects/ambuild/ambuild2/main.cpp -o main.o
         - /home/dvander/projects/ambuild/ambuild2/main.cpp
     - hello/helpers.o
       - [gcc] -> c++ -H -c /home/dvander/projects/ambuild/ambuild2/helpers.cpp -o helpers.o
         - /home/dvander/projects/ambuild/ambuild2/helpers.cpp
$ ambuild --show-steps
mkdir -p hello
task 0: [gcc] -> c++ -H -c /home/dvander/projects/ambuild/ambuild2/main.cpp -o main.o
  -> hello/main.o
task 1: [gcc] -> c++ -H -c /home/dvander/projects/ambuild/ambuild2/helpers.cpp -o helpers.o
  -> hello/helpers.o
task 2: c++ main.o helpers.o -o hello
  -> hello/hello

괜찮아 보이니 이제 빌드하자.

$ ambuild
mkdir -p hello
Spawned task master (pid: 15563)
Spawned worker (pid: 15564)
Spawned worker (pid: 15565)
[15564] c++ -H -c /home/dvander/projects/ambuild/ambuild2/helpers.cpp -o helpers.o
[15565] c++ -H -c /home/dvander/projects/ambuild/ambuild2/main.cpp -o main.o
[15565] c++ main.o helpers.o -o hello
[15565] Child process terminating normally.
[15564] Child process terminating normally.
[15563] Child process terminating normally.
Build succeeded.
$ ./hello/hello
Hello!

AMBuild는 각 C++ 바이너리를 각자의 폴더를 생성하여 넣는다. 예를 들어 정적 라이브러리인 egg.a, 공유 라이브러리인 egg.so, 실행 파일인 egg를 같은 폴더에 빌드했다고 치면 각자의 폴더를 나눠 빌드를 수행한다. 그렇게 생성된 바이너리 파일의 경로는 아래와 같다.

  • egg.a/egg.a
  • egg.so/egg.so
  • egg/egg

이걸 통해서 같은 파일들을 여러 번 다시 빌드하는 복잡한 상황을 만들 수 있다.

패키징

프로젝트를 빌드해봤다면 이제 패키징을 위해 빌드 스크립트를 추가해보자. 배포를 위해 아래의 파일을 폴더에 담아 zip, tar를 만들고 싶다면

  • README.txt, our readme
  • hello, 최종 바이너리

가장 먼저, 스크립트에 배포 폴더를 만드는 단계를 추가해야 한다.

dist_folder = builder.AddFolder('dist')

AddFolder는 의존성 그래프의 한 노드를 반환하므로 이걸로 추후 단계를 위한 입력으로써 사용이 가능하다.
그러면 이제 파일을 복사할 수 있다.

outputs = builder.Add(program)
 
folder = builder.AddFolder('dist')
builder.AddCopy(os.path.join(builder.sourcePath, 'README.txt'), folder)
builder.AddCopy(outputs.binary, folder)

README.txt는 소스 트리에서 직접 복사해올 수 있다. 실행파일을 복사하기 위해서는 builder.Add()의 반환 값을 사용할 수 있다. 경로를 직접 만드는 것도 가능하지만 이미 생성해둔 의존성 오브젝트를 이용하는게 더 편리하다.
이제 빌드를 하면 이렇게 된다.

[5952] cp "/home/dvander/projects/ambuild/ambuild2/README.txt" "./dist/README.txt"
Spawned worker (pid: 5953)
[5952] c++ -H -c /home/dvander/projects/ambuild/ambuild2/helpers.cpp -o helpers.o
[5954] c++ -H -c /home/dvander/projects/ambuild/ambuild2/main.cpp -o main.o
[5954] c++ main.o helpers.o -o hello
[5954] cp "hello/hello" "./dist/hello"

README.txt은 소스코드와의 의존성이 있지 않아 복사할 때에 다른 작업과 병렬적으로 수행할 수 있고 변경되지 않았다면 복사하지 않는다. 그러나 hello/hello의 복사는 마지막에 이뤄져야 한다. 그러면 이렇게 성공된 것을 볼 수 있다.

$ ls -l dist/
total 12
-rwxr-xr-x 1 dvander dvander 7036 Oct 16 22:32 hello
-rw-r--r-- 1 dvander dvander   23 Oct 16 22:32 README.txt

tar나 zip과 같은 명령어를 수행하는 단계를 추가할 수도 있지만 명령어에 포함되는 모든 파일이 의존성이 있지 않을 경우에 명령어의 순서가 뒤죽박죽 섞여버릴 수도 있어 복잡하다.

다중 스크립트

큰 규모의 프로젝트는 보통 하나 이상의 스크립트를 사용해야 할텐데 AMBuild가 이런 빌드 스크립트를 둥지 형태로 짤 수 있게 하여 한 스크립트에서 다른 스크립트를 실행하는 것이 가능하게 한다. 각 스크립트에서는 내부적인 문맥으로 각자의 builder를 갖게 되는데 모든 작업은 하나의 문맥에서 생성되고 연결된다. 이걸 통해 AMBuild에서 빌드 스크립트가 바뀌어도 최소한의 빌드 스크립트만을 다시 분석을 할 수 있게 된다.
문맥은 기본적으로 소스 트리 내의 상대적으로 존재하는 폴더에 연결된다. 예를 들어 빌드 스크립트가 /source-tree/src/game/AMBuildScript에 있었다면 문맥은 src/game에 연결된다. 이 폴더 구조는 빌드 폴더랑 미러링되어 모든 작업은 해당 문맥에 연결된 폴더에서 일어나게 된다. 예를 들어서 패키징을 위해 스크립트를 따로 분리해보자. PackageScript:

# PackageScript
import os
 
builder.SetBuildFolder('dist')
builder.AddCopy(os.path.join(builder.sourcePath, 'README.txt'), '.')
builder.AddCopy(Hello.binary, '.')

그러면 이제 메인 AMBuildScript를 수정하자:

# AMBuildScript
cxx = builder.DetectCxx()
 
program = cxx.Program("hello")
program.sources = [
  'main.cpp',
  'helpers.cpp',
]
outputs = builder.Add(program)
 
builder.Build(
  ['PackageScript'],
  { 'Hello': outputs }
)

Build 메소드의 첫 번째 매개변수는 구동을 위한 스크립트 경로의 배열이다. 두 번째는 각 스크립트에 주어지는 전역 변수의 딕셔너리다. PackageScript가 소스 트리의 루트에 있으므로 기본적으로 빌드 폴더는 ‘.’이지만 이걸 수동으로 사용될 빌드 폴더로 덮어쓸 수 있다.
PackageScript가 구성 단계에서 분석되었으면 거기서 수행될 모든 작업들이 빌드 폴더 내의 dist 폴더에서 일어나게 자동으로 구성되므로 이제 '.'가 아닌 './dist/'를 참조하게 한다.

만약 하나의 스크립트만을 사용한다면 Build 메소드를 대신 사용할 수도 있다. 이것도 스크립트에서 값을 반환하는데 예를 들어:

# AMBuildScript
folders = builder.Build('MakeFolders')
# MakeFolders
folders = [
  builder.AddFolder('egg'),
  builder.AddFolder('yam'),
  builder.AddFolder('plant'),
]
 
# Magic variable; assigning to "rvalue" will propagate the value
# back up to Build().
rvalue = folders

이렇게 변수들을 다른 스크립트에 보내는 것이 가능하다. (전역 변수로 취급된다.) 또는 builder 오브젝트에 첨부되어 전파할 수 있도록 하는 것이 후속으로 넣은 모든 스크립트를 다시 감지하고 구성하지 않아도 되서 유용하다. 더 많은 정보는 AMBuild API에 있다.
예를 들어 컴파일러 오브젝트를 넘기기 위해서는 이렇게 사용할 수 있다.

# AMBuildScript
builder.cxx = builder.DetectCxx()

대신에 이 방법은 다중 아키텍쳐 빌드에서 작동하지 않으므로 (x86과 x86_64 바이너리들) 컴파일러 목록을 대신 사용해야 한다.

커스텀 옵션들

Python의 optparse 모듈을 통해 구성 단계에 커스텀 옵션을 넣는 것이 가능하다. AMBuild에서 생성한 기본 configure.py을 다시 보면:

# vim: set sts=2 ts=8 sw=2 tw=99 noet:
import sys, ambuild2.run
 
parser = run.BuildParser(sys.path[0], api='2.2')
parser.Configure()

argparse.ArgumentParser의 인스턴스인 parser 오브젝트에는 options 속성이 있다. 여기에 추가할 수 있는데 :

# vim: set sts=2 ts=8 sw=2 tw=99 noet:
import sys, ambuild2.run
 
parser = run.BuildParser(sys.path[0], api='2.2')
parser.options.add_argument('--enable-debug', action='store_true', dest='debug', default=False,
                            help='Enable debugging symbols')
parser.options.add_argument('--enable-optimize', action='store_true', dest='opt', default=False,
                            help='Enable optimization')
parser.Configure()

options은 어느 builder 오브젝트든 접근할 수 있다. 따라서 이렇게도 가능하다:

if builder.options.debug:
  cxx.cflags += ['-O0', '-ggdb3']
  cxx.cdefines += ['DEBUG']
if builder.options.opt:
  cxx.cflags += ['-O3']
  cxx.cdefines += ['NDEBUG']
약한 의존성

가끔 별개의 페이즈에서 일어나는 빌드 단계를 강제하는게 유용할 수 있다. 그러나 AMBuild의 목적인 의존성을 완벽하게 나타내고 그 그래프를 정확하게 유지하는 것과 수동으로 작업의 순서를 조작하는 행위가 없게 하는 것에 대조되는 일이 되지만 그래프를 구성하는 방법을 완화해야 할 상황이 있긴 하다.

예를 들어서 “헤더 생성” 작업에서 의존성을 만들게 되면 생성되는 새로운 헤더들이 프로젝트 내의 모든 소스 파일을 다시 컴파일 하게 할 수도 있다. (그 헤더를 안쓰는 소스 파일까지도 말이다!) 뿐만 아니라 각 개별의 생성된 헤더에서 의존성을 생성하면 아주 큰 의존성 그래프를 얻게 된다. (50개의 include와 800개의 소스파일이 있다면 80,000개의 의존성 링크가 생긴다.) AMBuild는 tup이 했던 것처럼 첫번째 문제를 해결한다. 두 번째는? 이건 추후에 다뤄보자.

우선 약한 의존성에 대해 알 필요가 있다. 약한 의존성은 이론적으로는 존재하여 작업 순서에는 있지만 damage 전파가 되지 않는다. 예를 들면 hello.cppgenerated.h에 약한 의존성을 가졌다고 했을 때, hello.cpp#include "generated.h" 하지 않았다면 헤더에 변경이 없었을 때는 hello.cpp에 대한 빌드가 다시 이뤄지지 않아야 한다. 하지만 hello.cpp에서 generated.hinclude 하도록 변경했다면 약한 종속성이 이 작업들이 올바른 순서로 이뤄지도록 보장한다. (AMBuild에서 명백한 의존성 없이 생성된 파일에 의존하는 것은 불가능하다.) 약한 의존성은 강한 의존성으로 업그레이드 할 수도 있고 나중에 #include가 제거되었을 때 다시 다운그레이드 할 수도 있다.
예시로 두 단계를 수행하자. 헤더들을 생성하고 컴파일을 진행하기 위해 우선 셸 멸령어를 추가하고 출력을 전역 변수에 저장한다.

generated_headers = builder.AddCommand(
  argv = ['python', os.path.join(builder.buildPath, 'tools', 'buildbot', 'generate_headers.py')],
  inputs = [os.path.join(builder.sourcePath, '.hg', 'dirstate')],
  outputs = ['sourcemod_auto_version.h']
)

다른 빌드 스크립트에서 headers 오브젝트를 통해 소통한다고 가정하여 추가한다면:

library = cxx.Library('cstrike')
library.sources = ['cstrike.cpp', 'smsdk_ext.cpp']
library.sourcedeps += generated_headers
builder.Add(library)

그리고 이제 생성된 헤더에 변경 사항이 생기면 사용하는 라이브러리의 소스를 다시 컴파일 해야한다고 판단하여 컴파일은 헤더가 생성된 후에 진행하게 된다.

많은 소스 그룹들

프로젝트에서 복잡한 컴포넌트들을 가져야 할 경우가 있다. 예를 들어 다른 부분에서는 쓰이지 않지만 코드의 일부분에 어떤 컴파일 옵션을 넣어야 되는 경우는 모듈을 사용하여 해결할 수 있다. 예를 들면 프로젝트 루트는 이렇게 될 수 있다:

program = cxx.Program('sample')
 
builder.Build(['cairo/AMBuild', 'gtk/AMBuild'], {
  'program': program
})
 
builder.Add(program)

그러면 cario/AMBuild에서 이렇게 할 수 있다.

module = program.Module(builder, 'cairo')
module.sources += [
  'file.cc',
]
module.cxxflags += ['-Wno-flag-needed-for-cairo']

이제 이 모듈은 전체 바이너리의 일부분으로써 빌드하게 된다.

재구성

두 가지 이유로 재구성이 생길 수 있다. 첫 번째는 빌드의 속성이 바뀐 경우로 이미 최적화했던 빌드를 디버그 빌드로 구성한 경우에 생길 수 있다. 다른 하나는 빌드 스크립트가 바뀐 경우로 AMBuild에서 이전 구성 옵션을 사용하여 자동으로 다시 구성한다.

출력 정리

재구성이 일어나면 AMBuild는 새로운 의존성 그래프를 생성한 후 이전 의존성 그래프와 합친다. 이전 그래프에서 생성된 파일 중에 새로운 그래프에서 존재하지 않아 생성이 되지 않을 것은 빌드의 일관성과 변질되지 않는 것을 항상 유지 하기 위해 파일 시스템에서 삭제된다.
예를 들면 우리의 오리지널 스크립트는 오브젝트 폴더에서 이렇게 보일텐데:

-rw-rw-r-- 1 dvander dvander  933 Nov 11 02:26 goodbye.o
-rw-rw-r-- 1 dvander dvander 1232 Nov 11 02:26 helpers.o
-rwxrwxr-x 1 dvander dvander 8509 Nov 11 02:26 sample

AMBuildScript에서 ‘goodbye.cpp‘를 주석 처리하고 빌드해보자.

Reparsing build scripts.
Checking CC compiler (vendor test gcc)... ['cc', 'test.c', '-o', 'test']
found gcc version 4.7
Checking CXX compiler (vendor test gcc)... ['c++', '-fno-exceptions', '-fno-rtti', 'test.cpp', '-o', 'testp']
found gcc version 4.7
Removing old output: sample/goodbye.o
Spawned taskmaster (pid: 41899)
Spawned worker (pid: 41900)
[41900] c++ helpers.o -o sample
Build succeeded.

당연하게도 goodbye.o는 사라진다.

-rw-rw-r-- 1 dvander dvander 1232 Nov 11 02:26 helpers.o
-rwxrwxr-x 1 dvander dvander 8473 Nov 11 02:28 sample

최소 재분석

보통 의존성 그래프와는 다르게 AMBuild의 스크립트들은 서로 의존할 수 있어 그래프 오브젝트를 루트 스크립트로 다시 전파하고 그걸 또 순환해버리는 경우가 있을 수 있는데 이런 순환으로 인해 스크립트 중에 하나가 변경되면 전부를 다시 분석해야 하는 경우가 생긴다. 이런 최소 재분석 알고리즘이 실패하는 상황은 피하는 것이 상당히 어려우니 이 경우에 AMBuild에서는 그냥 모든 것을 재분석한다.
몇 백개의 빌드 스크립트가 있는 큰 프로젝트에서 이걸로 인한 성능 상의 병목 현상이 생긴다면 구성을 최소화하여 구현해야 한다. 빌드 스크립트 간의 임의의 데이터 이동을 허용하지 않는 더 제한적인 API로 전환해야 할 수도 있다. 문제를 해결하기 위해선 AMBuild가 스크립트의 상호 의존성을 확인할 수 있는 함수가 필요할 수 있다.
그냥 모든 것을 재분석 했어도 AMBuild에서는 바뀐 작업을 삭제하거나 다시 생성하기만 하는데 여기서 하나의 cpp 파일을 소스 목록에 추가했다면 전체 재분석의 결과로 파일이 빌드되고 해당 바이너리가 다시 연결된다. (물론 해당 바이너리에 의존하던 모든 작업도 마찬가지다.)

리팩터링

빌드 스크립트를 바꾸는 경우, 실제로 의존성 그래프를 변경하는지 보는 것이 좋다. 리팩터링을 거쳤거나 과도한 재빌드 문제를 추적할 때는 Tup이라면 ‘리팩터링’ 빌드로 손쉽게 대처할 수 있고 AMBuild와 같은 부류도 이런 기능을 지원하기는 하지만 아직 모든 문제를 잡아낼 순 없다.
의존성 그래프가 바뀐 상태에서 리팩터링 빌드를 한 경우는 아래와 같다:

dvander@linux64:~/temp$ ambuild --refactor obj-linux-x86_64
Reparsing build scripts.
Checking CC compiler (vendor test gcc)... ['cc', 'test.c', '-o', 'test']
found gcc version 4.7
Checking CXX compiler (vendor test gcc)... ['c++', '-fno-exceptions', '-fno-rtti', 'test.cpp', '-o', 'testp']
found gcc version 4.7
New output introduced: sample
Failed to reparse build scripts.

글을 원래 참고만 할 목적으로 짧게 적으려고 했다가 그냥 모두 받아적어버렸다..
API는 실제로 써보면서 필요한 것만 정리할 예정이다.


답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다