Using Jam

제목을 “Jam 소개”라고 적으려다가, 이미 잘 정리된 문서가 있는데 또 다시 전체를 정리하는 건 별로인 듯해서, 경험상 중요하다고 기억하는 것 위주로 적었다. 역시나, 다시 읽어보니 너무 기초적인 개념과 너무 세부적인 문제만 있고 메뉴얼에서 찾아볼만 한건 다 빠져서 배우는 용도로는 사용할 수 없겠다.

Hello, world!

Jamfile을 만든다. 단순한 경우, 아래 한줄이면 충분.

Main hello : hello.c ;

Jam을 실행한다.

$ jam

알아둘 것은

  • Jam binary는 Jambase라는 jam 문법으로 기술된 텍스트를 포함하고 있다.
  • Jambase에는 위의 Main을 포함한 기본적인 rule들이 정의되어 있다. (일종의 standard library)
  • 처음 jam이 실행되면 Jambase를 실행한다.
  • Jambase는 마지막에 현재 디렉토리의 Jamfile을 include하면서 끝난다.

공백의 중요성

모든 token은 공백으로 구분되어야 한다. 여기엔 콜론, 세미콜론 등의 구분자도 포함된다. 예를 들어,

Main prog : main.c ;

가 맞는 표현이고,

Main prog: main.c ;

이나

Main prog : main.c;

는 틀린 표현.

기본적으론 jam이 내부적으로 lex가 아닌 아주 단순하고 빠른 custom scanner를 사용하기 때문인데 (parsing은 yacc을 씀)

  1. 구분자를 포함한 문자열을 직접 표현할 수 있고 (구분자로만 이루어진 문자는 따옴표안에 표현할 수 있다. e.g., ":")
  2. (python의 indent 강제에 의한 효과처럼) 문장이 아주 regular해진다

는 약간의 장점이 있지만, 처음에 가장 많이 실수하는 부분. 특히 문법상 세미콜론을 빠뜨리면 뒤의 문장이 앞 문장의 인자의 일부로 인식되어 버려서 엉뚱한 곳에서 error가 발생하거나 해서 찾기 어려운 경우가 생긴다. 가벼운 건 좋지만 과연 이렇게까지 했어야 했는지는 의문. But, 익숙해지면 오히려 읽기 좋다는 생각도 든다.

한줄의 의미

Main prog : main.c data.c ;

위에서

  • Main은 Jambase에 정의된 rule 이름이다. 위의 한줄은 Main이라는 rule의 호출.
  • 인자는 콜론으로 구분된다. prog가 첫번째 인자, main.c data.c가 두번째 인자이다.
  • 모든 문자열은 공백으로 구분된 list이다. 즉, 두번째 인자는 main.cdata.c로 이루어진 list.

다음 Makefile의 예와 비교해 보자.

prog: main.o data.o
        cc -o prog main.o data.o

main.o: main.c common.h
        cc -c -o main.o main.c

data.o: data.c common.h
        cc -c -o data.o data.c

Main은 위의 Makefile과 동일한 방식으로 progmain.c, data.c와의 관계를 정의하는 rule이다.

이중 첫번째 부분만 Main을 사용하지 않고 jam으로 표현하면 다음과 같다.

rule Link prog : objs { Depends $(prog) : $(objs) ; }
actions Link { cc -o $(1) $(2) }

Link prog : main.o data.o ;

위는 jam의 기본이 되는 rule과 action의 예로, Makefile의 해당 부분과 비교해보면 그 의도를 파악할 수 있다.

  • Rule은 dependency나 변수 설정의 유사한 패턴을 표현
  • Action은 같은 패턴의 command들을 표현
  • Rule의 호출 = action의 지정 (둘 중 하나만 있으면 됨, 둘 다 없으면 error)
  • Action은 (dependency에 의해) 첫번째 인자를 update해야 할 때 실행된다.
    • 여러 action이 지정되면 모두 실행
    • 두번째 인자까지는 action을 기술할때 쓰일 수 있다.
    • 두번째 인자를 포함한 나머지 안자들은 자체로는 dependency와 무관하다.
  • Depends는 jam의 built-in rule로 실제 dependency를 지정한다.

위와 같이 정의함으로써, 이후에는 Link의 호출만으로 다른 target들에 대한 유사한 dependency와 command를 설정할 수 있게 된다. (개인적으로 처음에 문서로 jam을 공부하면서 놓치기 쉬웠던 부분이 바로 이 rule과 action의 연관 방식)

이해에 필요한 부분만 요약하면 Main은 다음과 같은 rule과 action들로 정의된다.

rule Main prog : srcs {
  MainFromObjects $(prog) : $(srcs:S=.o) ;
  Objects $(srcs) ;
}

rule MainFromObjects prog : objs {
  Depends $(prog) : $(objs) ;
  Link $(prog) : $(objs) ;
}
actions Link { cc -o $(1) $(2) }

rule Objects srcs {  for src in $(srcs) { Object $(src:S=.o) : $(src) ; }
rule Object obj : src { Cc $(obj) : $(src) ; }
rule Cc obj : src { Depends $(obj) : $(src) ; }
actions Cc { cc -c -o $(1) $(2) }

참고로,

  1. $(src:S=.o)는 suffix를 .o로 대체한 문자열을 의미
  2. 생략된 부분 때문에 위에서는 의미 없어보이는 rule들도 있지만, 실제로는 각각에 추가적인 역할이 있다.

Automatic dependency tracking

자세히 읽어봤다면, 앞의 내용만으론 main.odata.ocommon.h에 대한 dependency가 지정되지 않는다는 걸 알아차렸을 것이다. 실제 Object rule은 대략 다음과 같은 부분을 포함한다.

rule Object obj : src {
  Cc $(obj) : $(src) ;
  HDRSCAN on $(src) = "^[ \t]*#[ \t]*include[ \t]*[<\"]([^\"]*)[\">].*$" ;  # \t는 실제 tab으로 입력
  HDRRULE on $(src) = HdrRule ;
}

rule HdrRule src : hdrs {
  Includes $(src) : $(hdrs) ;
  HDRSCAN on $(hdrs) = $(HDRSCAN) ;
  HDRRULE on $(hdrs) = $(HDRRULE) ;
}

위에서 HDRSCAN, HDRRULE은 target-specific built-in variable로 다음과 같은 특수한 역할을 한다. HDRSCANHDRRULE이 둘 다 설정된 target에 대해서,

  • HDRSCAN의 egrep pattern을 모두 찾아서
  • HDRRULE의 rule을 호출. 이때,
    • 첫번째 인자는 target
    • 두번째 인자는 발견된 pattern들에서 괄호안에 match된 것들

위의 HdrRule에서 사용된 IncludesDepends와 마찬가지로 built-in rule로 Includes src : hdr ;src에 dependency가 있는 모든 target은 hdr에도 dependency가 있음을 의미한다. (main.ccommon.h에 의존하는 것이 아니라, main.ocommon.h에 의존)

보다시피 C, C++외에도 임의의 언어로 확장할 수 있도록 유연하게 되어 있다.

Procedural language

문법 상으로는 make와 유사성을 가지지만, 작성해보면 차이점이 많은데 그 중 하나가 make가 기본적으로 (c preprocessor, tex, m4 등과 같은) macro 언어임에 반해, jam은 procedural 언어라는 점이다. 즉, 모든 변수와 rule, action은 문장을 해석할 때의 것이 사용된다. (rule/action의 내용만은 정의시 해석되지 않고, 호출/실행시 풀림)

예를 들어,

X = 1 ; A = $(X) ; X = 2 ;

와 같은 문장이 있다면, A는 1이지만 (GNU make의 :=을 사용하지 않는 경우) make에서는 variable expansion이 나중에 되기 때문에 2가 사용된다.

이는 변수의 선언 시점이나, rule의 호출 순서가 중요할 수 있다는 것을 의미한다. (기본적으로 rule들이 참조하는 변수를 중간에 바꾸지 않으면 dependency graph는 달라지지 않겠지만..)

처리 과정

Jam의 전체적인 동작 순서는 다음과 같다.

  1. Parsing: dependency graph 구성 및 action 설정
  2. Binding: dependency graph의 각 target에 해당하는 file을 찾고 시간을 기록한다.
  3. Updating: update가 필요한 target들에 대한 action을 실행한다.

Binding에는 target-specific built-in variable인 SEARCHLOCATE가 사용된다. SEARCH는 말 그대로 찾을때 쓰이고, LOCATE는 없는 target을 새로 만들때의 위치. LOCATE가 우선한다. SEARCH에서 못찾으면, 현재 디렉토리 기준의 상대 경로에서 찾는다.

어쩌면 당연해 보이는 위의 순서가 미묘하고 중요한 이슈들을 가지고 온다.

  • Binding시에 없는 file은 updating때 생겨도 dependency graph를 바꾸지 못한다.

    예를 들어, 생성되는 source가 아직 없는 경우 header rule이 실행되지 않기 때문에 include하는 header 파일들에 대한 dependency는 자동으로 만들어지지 않는다. 보통은 문제가 되지 않을 수 있지만, include되는 header가 다시 생성되야 하는 파일이었다면 dependency가 없기때문에 만들어지지 않고, 따라서 source의 컴파일이 실패하게 된다.

    이를 해결하기 위해서는 소스 파일을 생성하는 rule에서 생성될 source의 dependency를 예측해서 기록해야 한다.

  • Updating시 action을 실행했는데 target이 바뀌지 않은 경우라고 해도 (update시에는 다시 binding하지 않기 때문에) 여전히 이 target에 의존하는 다른 target들은 update한다.

Directory tree 구성

다음과 같은 디렉토리 구조를 가정하자.

  • src
  • src/common
  • src/server
  • src/server/tests
  • src/client
  • src/client/tests

여기서 src를 최상위 디렉토리로 하면, 예를 들어 src/Jamfile은 다음과 같이 작성한다.

SubDir TOP  ;

SubInclude TOP common ;
SubInclude TOP server ;
SubInclude TOP client ;

비슷하게 src/server/Jamfile은 (예를 들어 common의 library가 필요하다고 가정할때) 다음과 같이 작성한다.

SubDir TOP server ;

SubInclude TOP common ;
SubInclude TOP server tests ;

위의 예에서

  • SubDir은 Jambase에 정의된 rule로 Jamfile의 가장 처음에 불려야 한다.
  • SubDir은 src/Jamrules가 있는 경우 먼저 include하고 SUBDIR 변수를 포함한 환경을 설정한다.
  • SubInclude를 실행하면 인자로 주어진 디렉토리의 Jamfile을 include한다. 이 Jamfile의 시작도 SubDir이기 때문에 이 후로는 관련 변수들이 변경된다. 따라서, SubInclude 이후에 문장을 넣는건 거의 모든 경우 잘못된 사용이다.

SUBDIR변수는 jam을 실행한 디렉토리로부터 현재 Jamfile이 있는 디렉토리로의 상대 경로가 설정된다.

Grist

서로 다른 디렉토리에 존재하는 동일한 이름의 다른 target이 하나의 target으로 인식되는 것을 막기위해서 사용되는 것이 “grist”이다. Grist는

  • 비교할때는 target 이름의 일부처럼 사용되지만
  • Binding할 때 파일 이름으로는 사용되지 않는

target의 보이지 않는 이름이다. (즉, grist가 다른, 이름이 같은 파일은 다른 target)

앞의 SubDir이 디렉토리 별로 grist를 설정하고 Jambase의 나머지 rule들은 binding되야 하는 target들에 항상 grist를 붙이도록 되어 있다.

이로 인해 또한가지 주의할 점이 생기는데, SubDir을 사용하는 경우 예를 들어 별도로 main.o라는 object의 target-specific variable CFLAGS를 설정하려고 하면

CFLAGS on main.o = -DXYZ ;

가 아닌

CFLAGS on [ FGristFiles main.o ] = -DXYZ ;

와 같이 grist를 지정해야 한다.

마치면서

처음 배울때 중요한 control flow, conditional문 등의 문법, variable expansion과 modifier, 몇개의 built-in rule과 Jambase에서 제공하는 Main, Library, LinkLibraries 등의 주요 rule들 등등은 메뉴얼에 있는 거라 죄다 빼먹었다.

문법을 포함한 나머지 내용은 다음을 참조할 것.

  • Jam.html: Jam의 문법, built-in rule, built-in variable에 대한 설명
  • Jambase.html: Jambase에서 제공하는 rule, target, variable에 대한 설명