Posts Tagged ‘Makefile’

h1

Rakefile basic

May 20, 2008

Rakefile은 Makefile과 비슷한 역할을 하는, Ruby script입니다. 따라서 Ruby라는 언어의 강력한 기능들을 그대로 가져다 쓸 수 있다는 장점이 있습니다. 단, Ruby를 알아야 제대로 사용할 수 있겠죠. Makefile을 make라는 명령어로 실행하듯이, Rakefile은 rake라는 명령어로 실행합니다. Rakefile 작성법을 Makefile 작성법과 비교하며 살펴보도록 하겠습니다. Makefile의 기본적인 작성법은
Target: Dependency list
[Tab] Command

였죠. Rakefile도 유사합니다. 단, Ruby syntax를 사용하죠. 기본적인 작성법은 다음과 같습니다.
task :name = [:prereq1, :prereq2] do
    Command
end

Makefile에서 Target에 해당하는 것이 Rakefile의 task입니다. 잘 살펴보면 task라는 함수명과 Hash, Block 두 개의 argument로 이루어진 구조라는 것을 알 수 있습니다. Hash의 key는 target이 되고 value는 prerequisites (dependency list)가 됩니다. Block은 실행해야 할 명령들로 이루어집니다. 특별히 compile하는 경우와 같이 파일을 작성하는 task의 경우에는
file "name" = ["prereq1", "prereq2"] do
    Command
end

와 같이 file task를 사용합니다. Command 부분에서 ‘name’ 또는 dependency list (prereq1, prereq2, … )를 사용하고 싶을 때는
file "name" = ["prereq1", "prereq2"] do |t|
sh “f77 -o #{t.name} #{t.prerequisites.join(’ ‘)}”
end

과 같이 사용하여 f77 -o name prereq1 prereq2와 같은 결과를 얻을 수도 있습니다.

그럼 앞에서 만들었던 Makefile과 같은 기능을 하는 Rakefile을 만들어 비교해 보겠습니다. 앞에서 만들었던 Makefile은 다음과 같고,

01: # target: dependency list
02: # [tab] command
03: F77=gfortran
04:
05: all: main
06:
07: main: main.o sub1.o sub2.o
08:         $(F77) -O2 -o main main.o sub1.o sub2.o
09: main.o: main.f
10:         $(F77) -O2 -c main.f
11: sub1.o: sub1.f
12:         $(F77) -O2 -c sub1.f
13: sub2.o: sub2.f
14:         $(F77) -O2 -c sub2.f
15: clean:
16:         rm main main.o sub1.o sub2.o

이에 해당하는 Rakefile은 다음과 같습니다.


f90='gfortran'

task :default => ['main.e']
file 'main.e' => ['main.o','sub1.o','sub2.o'] do |t|
    sh "#{f90} -o #{t.name} main.o sub1.o sub2.o"
end

file 'main.o' => ['main.f'] do
    sh "#{f90} -c main.f"
end
file 'sub1.o' => ['sub1.f'] do
    sh "#{f90} -c sub1.f"
end
file 'sub2.o' => ['sub2.f'] do
    sh "#{f90} -c sub2.f"
end

require 'rake/clean'
CLEAN.include('*.o')
CLOBBER.include('main.e')

task :default 부분은 Makefile에서 all 이라는 target을 지정해서 사용했던 것과 같은 역할을 합니다. 단, Rakefile에서는 default task가 맨 처음에 나올 필요가 없습니다. 파일 내 아무데나 나와도 잘 인식합니다. 중간 부분은 Makefile과 매우 유사하므로 특별한 설명이 필요 없겠죠? 뒤에 있는 clean task는 rake에 이미 지정되어 있는 task입니다. 사용하기 위해서는 ‘rake/clean’을 불러옵니다. rake clean을 실행하면 CLEAN에 포함된 파일들을 지워주고 rake clobber를 실행하면 CLOBBER와 CLEAN에 지정된 파일들을 모두 지워줍니다. 위에서 볼 수 있는 것처럼, 최종 결과 파일만 CLOBBER에 포함시키고 중간에 생성되는 파일들은 CLEAN에 포함시키면 편리하게 사용할 수 있습니다.

Makefile에서는 확장자법칙을 이용해 편리하게 compile할 수 있었죠? Rakefile에도 같은 기능이 있습니다. 비교해볼까요?

01: # $^ : dependency list
02: # $@ : target
03:
04: F77=ifort
05: FFLAG=-assume byterecl -O2
06: TARGET=main
07: OBJECTS=main.o sub1.o sub2.o
08:
09: all: $(TARGET)
10:
11: $(TARGET): $(OBJECTS)
12:         $(F77) -o $@ $^
13:
14: .SUFFIXES: .o .f
15: %.o: %.f
16:         $(F77) ${FFLAG} -c $^
17:
18: clean:
19:         rm $(TARGET) $(OBJECTS)

F90='ifort'
FFLAG='-assume byterecl -O2'
TARGET='main.e'
SRC=FileList['*.f']
OBJ=SRC.ext('o')

task :default => TARGET
file TARGET => OBJ do
    sh "#{F90} -o #{TARGET} #{OBJ}"
end
rule '.o' => '.f' do |t|
    sh "#{F90} #{FFLAG} -c #{t.source}"
end

require 'rake/clean'
CLEAN.include('*.o')
CLOBBER.include('main.e')

Rakefile에서는 rule이라는 함수가 Makefile의 확장자법칙과 같은 역할을 합니다. FileList 명령은 glob pattern (여기서는 ‘*.f’)을 받아들여서 해당하는 파일들의 목록을 만들어주고, FileList 객체의 ext method는 목록에 있는 파일들의 확장자를 원하는 확장자로 바꿔서 새로운 FileList를 만들어줍니다. 앞에서 dependency list를 불러올 때 t.prerequisites.join(' ')이라고 사용했었는데 여기서는 t.source라고 사용했습니다. 앞의 방법은 전체 dependency list를 문자열로 만들어주고(’ ‘을 이용하여 각각을 합치죠), 뒤의 방법은 dependency list의 첫 번 째 항목만 문자열로 만들어줍니다. 위의 예에서는 dependency list에 ‘.o’에 해당하는 ‘.f’ 파일 하나만 있으니까 t.source라고 사용해도 무관하겠죠?

Makefile에 없고 Rakefile에만 있는 기능 중 하나로, task에 설명을 달 수 있는 기능이 있습니다. task 또는 file task 바로 윗 줄에
desc "description"
이라고 설명을 추가해주면 rake -T라고 실행했을 때 설명과 함께 task 목록을 보여줍니다. Rakefile을 직접 보지 않고도 안에 무슨 task가 있는지 확인할 수 있는 유용한 기능이죠^^

더 자세한 내용은 다음의 site들을 참고하세요.
http://rake.rubyforge.org/
http://docs.rubyrake.org/

h1

Makefile basic

May 8, 2008

Makefile의 기초적인 사용법을 알아봅시다.

먼저, 다음과 같이 3개의 source 파일이 있을 때 컴파일하는 과정을 알아보겠습니다.

첫 번째 파일: main.f

1:       implicit none
2:       print *, 'main'
3:       call sub1()
4:       call sub2()
5:       end

두 번째 파일: sub1.f

1:       subroutine sub1()
2:       print*, 'sub1'
3:       end

세 번째 파일: sub2.f

1:       subroutine sub2()
2:       print*, 'sub2'
3:       end

main.f 파일에서 sub1.f와 sub2.f에 있는 subroutine들을 불러오는 매우 간단한 프로그램입니다. 여기서는 하나의 파일 안에 subroutine을 다 넣는 것이 더 편하지만, Makefile 연습을 위해 세 개의 파일로 나누어 놓았습니다.
이렇게 세 개의 파일을 가지고 실행 파일을 만들기 위한 명령은 다음과 같죠.
f77 -c -O2 -o main.o main.f
f77 -c -O2 -o sub1.o sub1.f
f77 -c -O2 -o sub2.o sub2.f
f77 -o main main.o sub1.o sub2.o

위 명령에서 ‘-c’ 옵션은 source code를 가지고 object file을 생성하라는 의미입니다. 각각의 source file들에 대해 object file을 생성하고 나중에 링크시켜서 실행파일을 만듭니다. ‘-O2′는 compile 할 때 optimization level 을 2로 하라는 의미, ‘-o 파일명’ 은 output file을 ‘-o’ 다음에 나오는 파일명으로 만들라는 의미입니다. 마지막 줄에서 세 개의 objective file들을 링크시켜서 ‘main’ 이라는 실행파일을 생성합니다. 실행 파일의 실행 결과는 다음과 같습니다.
$ ./main
main
sub1
sub2

잘 실행됩니다. 그런데 만약 sub1.f 파일을 수정했다면 어떻게 해야할까요? 다시 컴파일하기 위해서는
f77 -c -O2 -o sub1.o sub1.f
f77 -o main main.o sub1.o sub2.o

라고 수정한 파일만 다시 컴파일한 후, 다른 object file들과 링크시켜서 실행파일을 만들어야겠죠. 좀 귀찮습니다. 자동으로 할 수 있으면 좋겠죠. shell script를 하나 만들어서 처음의 컴파일 명령 4줄을 다 써 넣으면 자동으로 실행할 수 있습니다. 하지만, 그렇게 되면 sub1.f 파일을 고쳤을 때 main.f와 sub2.f 파일까지 새로 컴파일하게 됩니다. 프로그램이 간단할 때는 큰 문제가 없지만, 프로그램이 크고 복잡해지면 컴파일 시간이 오래 걸린다는 문제가 생기게 됩니다. Source code 내용에 따라서 특정 code는 다른 code보다 먼저 컴파일해야만 하는 경우도 생길 수 있습니다. 이런 경우에 편리하게 쓸 수 있는 프로그램이 바로 make입니다. make는 실행했을 때 현재 디렉토리에 있는 Makefile 이라는 파일을 찾아 build 작업을 수행합니다.

Makefile의 작성법은
Target: Dependency list
[Tab] Command

와 같습니다. Target은 만들고 싶은 대상, Dependency list는 Target을 만들기 전에 먼저 만들어져야 할 대상들, Command는 Target을 만드는 방법(command line 명령어)입니다. 한 가지 주의할 점은 Command 앞에는 Tab이 들어가야 한다는거죠. 그럼 위의 세 파일을 컴파일하기 위한 기초적인 Makefile을 살펴보겠습니다.

01: # target: dependency list
02: # [tab] command
03: F77=gfortran
04:
05: all: main
06:
07: main: main.o sub1.o sub2.o
08:         $(F77) -O2 -o main main.o sub1.o sub2.o
09: main.o: main.f
10:         $(F77) -O2 -c main.f
11: sub1.o: sub1.f
12:         $(F77) -O2 -c sub1.f
13: sub2.o: sub2.f
14:         $(F77) -O2 -c sub2.f
15: clean:
16:         rm main main.o sub1.o sub2.o

F77=gfortran 이라고 먼저 선언을 했습니다. 여기서 F77은 매크로(일종의 변수)입니다. 이런식으로 선언을 해두면 뒤에 $(F77)과 같이 필요할 때 불러서 쓸 수 있습니다. 컴파일러를 바꿀 때 gfortran 대신에 f77 이나 ifort 등으로 바꿔주면 되겠죠.

다음에 나오는게 all 이라는 Target입니다. Dependency list에는 main이 있고 Command는 없네요. Command line에서 make를 실행하면 현재 디렉토리에 있는 Makefile을 찾아 제일 처음에 나오는 Target만 실행합니다. make target1과 같이 실행하면 Makefile내에서 target1 이라는 Target을 찾아 실행합니다. 따라서 맨 처음 Target을 all 이라고 지정해두고 Dependency list에 자신이 만들고 싶은 Target들을 적어두면 make만 쳐서 원하는 Target들을 한 번에 만들 수 있겠죠. 전체적인 compile 과정은 다음과 같습니다.

  1. 제일 처음에 나오는 ‘all’ 이라는 Target을 만나서 Dependency list를 확인한다. ‘main’이라는 Dependency를 찾았다.
  2. Dependency를 만족하기 위해 ‘main’이라는 Target을 찾는다. 그리고 ‘main’의 Dependency list - ‘main.o’, ’sub1.o’, ’sub2.o’ 를 찾았다.
  3. ‘main.o’라는 Target을 찾아서 Dependency ‘main.f’를 찾고 현재 디렉토리에 ‘main.o’ 파일이 없거나 ‘main.o’ 파일의 수정 시간이 ‘main.f’ 파일의 수정시간보다 이전일 때
    gfortran -O2 -c main.f
    라는 Command를 실행한다. 그렇지 않은 경우에는 아무 것도 실행하지 않는다.
  4. ‘main.o’가 잘 만들어졌으면 다시 ‘main’ 이라는 Target으로 넘어가 ’sub1.o’, ’sub2.o’라는 Dependency를 같은 방법으로 만족하고 돌아온다.
  5. ‘main’의 Dependency 세 개가 다 만족되었으면
    gfortran -O2 -o main main.o sub1.o sub2.o
    라는 Command를 실행하여 ‘main’이라는 Target을 만든다.
  6. ‘main’이라는 Target이 만들어졌으면 ‘all’이라는 Target으로 돌아간다.
  7. ‘all’의 Dependency가 다 만족되었지만, Command가 없으므로 make가 끝난다.

‘clean’이라는 Target은 처음에 나오지도 않고 다른 Target의 Dependency 에도 들어가지 않으니 실행이 안 됩니다. 명령줄에서 make clean이라고 실행했을 때만 실행이 되죠. ‘clean’은 Dependency list가 비어있으니까 make clean이라고 실행하면 해당하는 Command를 항상 실행하게 됩니다. 보통 make로 생성된 파일들을 지우기 위해 ‘clean’이라는 Target을 만듭니다.

여기까지만 배우고 끝내기에는 아쉽습니다. Makefile에는 강력한 기능들이 많기 때문이죠. 몇 개만 더 살펴봅시다. 아래의 Makefile은 compiler과 compile option이 약간 바뀐 것 말고는 위의 Makefile과 같은 기능을 합니다.

01: # $^ : dependency list
02: # $@ : target
03:
04: F77=ifort
05: FFLAG=-assume byterecl -O2
06: TARGET=main
07: OBJECTS=main.o sub1.o sub2.o
08:
09: all: $(TARGET)
10:
11: $(TARGET): $(OBJECTS)
12:         $(F77) -o $@ $^
13:
14: .SUFFIXES: .o .f
15: %.o: %.f
16:         $(F77) ${FFLAG} -c $^
17:
18: clean:
19:         rm $(TARGET) $(OBJECTS)

Command 위치에서 ‘$^’는 Dependency list를 자동으로 입력해줍니다. 또한 ‘$@’는 Target 이름을 자동으로 입력해줍니다.

.SUFFIXES: .o .f
%.o: %.f
        $(F77) ${FFLAG} -c $^

는 확장자 규칙으로, .SUFFIXES: .o .f 는 ‘.o’ 라는 확장자와 ‘.f’라는 확장자를 특별히 중요하게 생각하라는 뜻입니다. 그 아래에 나오는 내용은 ‘.o’ 확장자를 가진 Target에 대해 ‘.f’ 파일을 이용한 Dependency 와 Command를 자동으로 생성해주는 기능을 합니다. 따라서 처음의 Makefile에 있었던 ‘main.o’, ’sub1.o’, ’sub2.o’ 라는 Target과 Dependency, Command를 자동으로 만들어줍니다. Build 과정이 복잡할 때 Makefile은 큰 힘을 발휘합니다.

다음에는 Makefile과 비슷한 역할을 하는, Rakefile을 만드는 법을 배워보겠습니다. Rakefile은 Ruby script로, Ruby라는 언어의 강력한 기능들을 그대로 가져다 쓸 수 있다는 장점이 있습니다. Rakefile을 만든 후에는 Python 언어로 만들어진 SConstruct 를 만드는 법도 배워보겠습니다.

더 자세한 내용을 알고 싶으신 분들은 임대영님의 GNU Make 강좌를 참고하세요.