카테고리 보관물: Computer

Unit number 자동 할당

포트란에서 파일을 열 때 파일에 번호(Logical unit number)를 할당하고 그 번호를 이용하여 파일 내용을 읽거나 파일에 출력을 하게 됩니다. 리눅스에서 다음 세 번호는 기본적으로 할당이 되어(입출력 스트림이 열려) 있습니다.

0: stderr
5: stdin
6: stdout

나머지 4바이트 정수 자료형으로 표현할 수 있는 양의 정수값은 사용자가 파일에 할당하여 사용할 수 있습니다 (컴파일러마다 범위는 차이가 있습니다). 보통은 사용하는데 문제가 없지만 하나의 프로그램에서 여러 개의 파일을 열 경우 앞에서 다른 파일에 할당하여 사용중인 번호를 피해 새로운 번호를 찾아야 합니다. 사용중인 번호를 피하기 위해 앞의 코드를 살펴본다든지, 파일 입출력 서브루틴을 위해 새로운 번호를 전달해줄 경우 귀찮다는 생각이 들게 됩니다. 예를 들면 다음과 같은 경우죠.

subroutine write_a_number(un, filename, n)
integer,intent(in):: un, n
character(len=*),intent(in):: filename
open(un,file=trim(filename))
write(un,*) n
close(un)
end subroutine

이럴 때 inquire함수를 사용하면 특정 번호가 사용중인지 알 수 있고, 따라서 새로운 번호도 자동으로 찾을 수 있습니다. 아래 서브루틴과 함수는 gpl의 포트란 모듈(module_base)에서 복사하였습니다. 파일 번호는 99번부터 10번까지만 사용하게 만들었습니다. 동시에 그 이상의 파일을 열 일은 없다고 생각하고 만들었는데, 많은 파일을 열어야 할 경우 do loop 범위를 조정하면 되겠습니다.

subroutine assign_un(un)
integer, intent(out) :: un
logical :: oflag
integer :: i
do i=99,10,-1
    inquire(unit=i,opened=oflag)
    if(.not.oflag) then
        un=i
        return
    endif
enddo
stop "Error: Logical unit assignment"
end subroutine

logical function un_opened(un) result(val)
integer,intent(in):: un
inquire(unit=un,opened=val)
end function

위의 서브루틴을 사용하면 앞에서 살펴본 write_a_number 서브루틴을 다음과 같이 고칠 수 있습니다. 서브루틴의 인터페이스가 좀 더 간단해졌고, 앞으로는 서브루틴을 사용할 때 파일 번호에 대해 고민할 필요가 없겠죠. 자동화의 간단한 예가 되겠습니다.

subroutine write_a_number_new(filename, n)
integer,intent(in):: n
character(len=*),intent(in):: filename
integer:: un
call assign_un(un)
open(un,file=trim(filename))
write(un,*) n
close(un)
end subroutine

Geophysical Prospecting Library

GPL (Geophysical Prospecting Library)

이 라이브러리는 제가 개인적으로, 대부분 직접 작성하여 사용하는 프로그램들이나 스크립트들을 모아놓은 라이브러리입니다. 연구를 위해 실용적으로 사용하는 잡동사니들의 모음으로, 반복되는 작업들을 자동화하는데 초점을 맞추고 작성한 라이브러리입니다. 내용은 수치해석 프로그래밍을 편리하게 하기 위한 포트란 모듈과 C 함수들, 컴파일을 쉽게 하기 위한 스크립트, 파일 수정 및 부분 추출 등을 위한 프로그램들, 결과 확인 및 논문 그림 그리기를 위한 스크립트들 등이 있습니다. 주로 포트란으로 작성하였고, 그 외에 프로그래밍 연습도 할 겸 C, 파이썬, 루비 등의 언어를 사용하였습니다.

라이브러리 정리 및 간단한 문서화를 위해 블로그를 통해 라이브러리의 기능을 하나 둘씩 공개하려고 합니다. 홈페이지를 방문해주신 누군가에게 도움이 되기를 바랍니다. 코드는 GitHub를 통해 공개하겠습니다.

설치 방법

설치 전에

gpl을 제대로 사용하기 위해서는 Fortran 및 C 컴파일러가 필요하고, Python (2.7)과 Ruby (1.9) 인터프리터가 필요합니다. 모든 gpl 기능을 원활히 사용하기 위해서는 탄성파 자료처리 패키지인 Seismic Un*xMadagascar, Python의 NumpyMatplotlib 라이브러리, SConstruct 그리고 GnuplotImageMagick이 필요합니다.

설치

1. GitHub에서 파일을 받습니다.
2. path_to_gpl/gpl/compiler.py에서 Fortran과 C compiler 관련 설정을 해줍니다.
3. make install을 실행합니다.
4. ~/.bash_profile (Mac에서는 ~/.profile)에 path_to_gpl/etc/env.sh의 내용을 추가해줍니다.

Rakefile 사용법

Rakefile 기본 사용법

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은 다음과 같고,

# target: dependency list
# [tab] command
F77=gfortran

all: main

main: main.o sub1.o sub2.o
    $(F77) -O2 -o main main.o sub1.o sub2.o
main.o: main.f
    $(F77) -O2 -c main.f
sub1.o: sub1.f
    $(F77) -O2 -c sub1.f
sub2.o: sub2.f
    $(F77) -O2 -c sub2.f
clean:
    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를 실행하면 CLOBBERCLEAN에 지정된 파일들을 모두 지워줍니다. 위에서 볼 수 있는 것처럼, 최종 결과 파일만 CLOBBER에 포함시키고 중간에 생성되는 파일들은 CLEAN에 포함시키면 편리하게 사용할 수 있습니다.

확장자 규칙

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

Makefile

# $^ : dependency list
# $@ : target

F77=ifort
FFLAG=-assume byterecl -O2
TARGET=main
OBJECTS=main.o sub1.o sub2.o

all: $(TARGET)

$(TARGET): $(OBJECTS)
    $(F77) -o $@ $^

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

clean:
    rm $(TARGET) $(OBJECTS)

Rakefile

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를 참고하세요.

예전에 다른 블로그에 올렸던 글인데, 이곳에 복사해둡니다.

포트란과 연산자 오버로딩

함수 오버로딩과 마찬가지로, 연산자 오버로딩도 객체지향 프로그래밍의 다형성과 관련된 개념입니다. 포트란 90에서 연산자 오버로딩을 사용하는 방법을 살펴보겠습니다. 아래 코드는 이차원 좌표 자료형과 두 개의 점을 더하는 함수 예제입니다.

module point2d_op

type point
    real x, y
end type

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3=add(p1,p2)

print*, p3%x,p3%y
end program

새로운 자료형을 정의했으니 새로운 자료형에 대응하는 더하기 연산도 따로 정의할 필요가 있습니다. 그런데 p3=add(p1,p2)와 같이 쓰는 것보다는 p3=p1+p2로 쓰는 것이 더 직관적이고 이해하기 쉽겠죠. 이 때 사용하는 것이 연산자 오버로딩입니다.

연산자 오버로딩

포트란에서는 interface문을 사용하여 이미 존재하는 연산자를 오버로드 하거나 새로운 연산자를 정의할 수 있습니다. 아래 예제는 + 연산자를 오버로드 하는 예제입니다.

module point2d_op

type point
    real x, y
end type

interface operator (+) 
    module procedure:: add 
end interface

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3 = p1 + p2

print*, p3%x,p3%y
end program

수학 연산자를 오버로딩할 때에는 수학적 정의에 합당하도록 또는 이해하기 쉽도록 정의하는 것이 좋습니다. 위의 add 함수를 - 연산자에 오버로딩하는 것도 가능하나 그렇게 되면 프로그래밍할 때 문제가 발생하겠죠.

새로운 연산자 정의

연산자를 오버로드하지 않고 새로 정의할 수도 있습니다. 새로 정의하는 연산자는 .name.과 같이 '.'으로 시작해서 '.'으로 끝나야 합니다. 아래 예제는 add 함수를 .add. 연산자로 정의한 경우입니다. p3=add(p1,p2) 대신 p3=p1.add.p2와 같이 사용한 것을 볼 수 있습니다.

module point2d_op

type point
    real x, y
end type

interface operator (.add.) 
    module procedure:: add 
end interface

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3 = p1 .add. p2

print*, p3%x,p3%y
end program

포트란과 함수 오버로딩

포트란과 객체지향 프로그래밍

포트란이라 하면 옛날 언어라고 생각하기 쉬운데, 포트란 언어도 시대의 흐름에 따라 계속 발전해 오고 있습니다. 특히, 2003버전부터는 클래스와 상속 등을 지원하는 객체지향 프로그래밍 언어라 할 수 있습니다. 안타깝게도, 한글로 된 포트란 서적이 별로 없고, 출판된 것도 95버전까지밖에 안 나와 있어서 앞으로 블로그에서 몇 가지 객체지향 프로그래밍과 관련된 기능들을 소개하고자 합니다.

포트란과 함수 오버로딩

함수 오버로딩이란 같은 이름의 서브프로그램(서브루틴, 함수, 메소드 등)이 인자에 따라 다른 기능을 하는 것을 말합니다. 객체지향 프로그래밍의 다형성과 관련된 개념인데, 포트란에서는 90버전에 도입된 interface문을 이용하여 적용 가능합니다.

아래 예제는 2차원 좌표라는 자료형을 정의하고 좌표들 사이의 거리를 구하는 함수를 구현한 예제입니다. 자료형을 정의할 때 single precision 자료형 point_sp와 double precision 자료형 point_dp 두 가지를 정의했고, 따라서 함수도 single precision용 distance_sp와 double precision용 distance_dp 두 가지를 정의했습니다.

module point2d

type point_sp
    real x, y
end type

type point_dp
    real(kind=8):: x,y 
end type

contains

    real function distance_sp(p1, p2) result(dist)
    type(point_sp), intent(in):: p1, p2
    dist=sqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function

    real(kind=8) function distance_dp(p1, p2) result(dist)
    type(point_dp), intent(in):: p1, p2
    dist=dsqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function 

end module

program test_point2d
use point2d
type(point_sp):: s1,s2
type(point_dp):: d1,d2
real:: dist_s
real(kind=8):: dist_d

s1%x=0.0 ; s1%y=0.0
s2%x=3.0 ; s2%y=4.0

d1%x=0.0d0 ; d1%y=0.0d0
d2%x=3.0d0 ; d2%y=4.0d0

dist_s=distance_sp(s1,s2)
dist_d=distance_dp(d1,d2)

print*, dist_s, dist_d
end program

위의 예제에서 두 개의 함수가 하는 일은 같지만, 정적 자료형 언어의 특성상 자료형에 따라 두 개의 함수를 정의해야 했습니다. 이 프로그램은 간단해서 별 문제가 없지만, 프로그램이 복잡해지면 이렇게 같은 기능의 다른 함수들로 인해 인터페이스가 복잡해지게 됩니다. 같은 기능의 함수들을 single/double과 같은 자료형에 상관없이 같은 이름으로 사용할 수 있다면 프로그래밍 인터페이스가 좀 더 단순해지겠죠. 위의 모듈에 아래의 interface 구문을 넣으면 distance_sp 함수와 distance_dp 함수를 모두 distance라는 이름으로 사용할 수 있습니다.

interface distance
    module procedure:: distance_sp
    module procedure:: distance_dp
end interface

위와 같이 선언해 놓으면 두 함수의 인자가 두 개의 point_sp와 두 개의 point_dp로 다르기 때문에 인자의 자료형을 가지고 어떤 함수를 사용해야하는지 판단할 수 있게 됩니다. 만약 두 함수의 인자가 동일하다면 interface문을 사용할 수 없습니다. 아래는 함수 오버로딩을 사용하도록 수정한 예제입니다.

module point2d

type point_sp
    real x, y
end type

type point_dp
    real(kind=8):: x,y 
end type

interface distance
    module procedure:: distance_sp
    module procedure:: distance_dp
end interface

contains

    real function distance_sp(p1, p2) result(dist)
    type(point_sp), intent(in):: p1, p2
    dist=sqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function

    real(kind=8) function distance_dp(p1, p2) result(dist)
    type(point_dp), intent(in):: p1, p2
    dist=dsqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function 

end module

program test_point2d
use point2d
type(point_sp):: s1,s2
type(point_dp):: d1,d2
real:: dist_s
real(kind=8):: dist_d

s1%x=0.0 ; s1%y=0.0
s2%x=3.0 ; s2%y=4.0

d1%x=0.0d0 ; d1%y=0.0d0
d2%x=3.0d0 ; d2%y=4.0d0

dist_s=distance(s1,s2)
dist_d=distance(d1,d2)

print*, dist_s, dist_d
end program

자료형에 상관없이 distance라는 함수를 이용하여 거리를 계산하고 있음을 알 수 있습니다. 물론 위에서 distance_spdistance_dp도 사용 가능합니다. 함수 오버로딩을 사용한다고 프로그램의 실행 속도가 빨라지지는 않습니다. 하지만 서브프로그램들의 인터페이스를 단순화하여 프로그래밍을 좀 더 편하게 할 수 있다는 장점이 있습니다.

Makefile 사용법

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

Makefile이 필요한 경우

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

첫 번째 파일: main.f

       implicit none
       print *, 'main'
       call sub1()
       call sub2()
       end

두 번째 파일: sub1.f

       subroutine sub1()
       print*, 'sub1'
       end

세 번째 파일: sub2.f

       subroutine sub2()
       print*, 'sub2'
       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 작성법

Makefile의 작성법은

Target: Dependency list
[Tab] Command

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

# target: dependency list
# [tab] command
F77=gfortran

all: main

main: main.o sub1.o sub2.o
    $(F77) -O2 -o main main.o sub1.o sub2.o
main.o: main.f
    $(F77) -O2 -c main.f
sub1.o: sub1.f
    $(F77) -O2 -c sub1.f
sub2.o: sub2.f
    $(F77) -O2 -c sub2.f
clean:
    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과 같은 기능을 합니다.

# $^ : dependency list
# $@ : target

F77=ifort
FFLAG=-assume byterecl -O2
TARGET=main
OBJECTS=main.o sub1.o sub2.o

all: $(TARGET)

$(TARGET): $(OBJECTS)
    $(F77) -o $@ $^

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

clean:
    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은 큰 힘을 발휘합니다.

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

예전에 다른 블로그에 올렸던 글인데, 이곳에 복사해둡니다.