zig build는 compile_commands.json을 직접 생성하지 않는다. 기존 모듈을 Zig 0.13에 맞게 수정해 빌드 단계 하나로 생성하도록 했다.
Zig에 내장된 빌드 시스템은 C 프로젝트를 컴파일할 때도 쓸 수 있다. 다만 문제가 하나 있다. 에디터의 clangd가 각 파일에 어떤 헤더 경로와 컴파일 옵션을 써야 하는지 몰라서, 코드 곳곳에 빨간 줄이 생기고 정의로 이동하는 기능도 작동하지 않는다.
clangd는 프로젝트의 compile_commands.json을 참고한다. CMake는 이 파일을 바로 내보낼 수 있지만, zig build에는 이를 위한 기본 단계가 없다.
다행히 빌드 그래프에서 필요한 정보를 추출해 compile_commands.json을 생성하는 모듈을 이미 만든 사람이 있었다. 하지만 예전 Zig API를 기준으로 작성된 모듈이었고, 내가 쓰는 버전은 0.13이었다. 그사이 빌드 시스템 API가 많이 바뀌어서 그대로는 컴파일되지 않았다. 그래서 이 모듈을 바탕으로 수정한 버전을 만들었다. 저장소는 xxxbrian/zig-compile-commands에 있다.
사용법
먼저 build.zig.zon에 의존성으로 추가한다. 특정 커밋의 tarball URL과 그에 맞는 해시를 지정하면 된다. 아래의 <commit>과 <hash>는 실제 값으로 바꿔야 한다.
이렇게 하면 zcc라는 단계가 추가된다. zig build zcc를 실행하면 프로젝트 루트에 compile_commands.json이 생성되고, clangd가 자동으로 찾아서 사용한다.
생성 원리
이 모듈은 빌드 그래프에서 C와 관련된 정보를 하나씩 clang 명령줄 인자로 바꾼다.
전달받은 각 타깃의 link_objects를 순회한다. C 소스 파일을 만나면 파일 경로와 해당 파일에 따로 지정한 컴파일 옵션을 기록한다. 시스템 라이브러리는 -l로 시작하는 인자로 바꾸고, libc나 libc++를 링크했다면 각각 -lc, -lc++도 추가한다. 헤더 디렉터리는 -I 인자로 바꾸며, 다른 타깃이 설치한 헤더의 디렉터리도 함께 넣는다. 또 타깃이 다른 타깃을 링크하고 있다면 그것도 리스트에 추가해 계속 순회한다. 따라서 최상위 실행 파일 타깃만 넘겨도 전체 정보를 모을 수 있다.
각 C 소스 파일은 최종적으로 compile_commands.json의 항목 하나가 된다. 여기에는 작업 디렉터리, 파일 경로, 출력 경로와 전체 인자 목록이 들어간다. clangd가 이해하는 것은 clang의 명령줄이므로, 인자 목록은 clang을 호출하는 형태로 작성한다.
Important
createStep은 관련된 모든 타깃의 설정을 마친 뒤 호출해야 한다. 모듈이 호출 시점에 빌드 그래프를 읽기 때문에, 설정이 끝나지 않은 타깃이 있으면 해당 헤더 디렉터리와 소스 파일을 읽지 못한다.
한 가지 더 있다. 생성 단계가 실행될 때는 createStep을 호출하며 넘겼던 인자를 직접 가져올 수 없다. 그래서 모듈에서는 전역 변수에 타깃 리스트를 저장해 두고, 나중에 실행되는 단계가 읽을 수 있게 했다.
Zig 0.13에 맞추기
이전 API에서 0.13으로 바뀌면서 달라진 부분은 주로 다음과 같다.
std.Build.CompileStep이 std.Build.Step.Compile로 바뀌었다.
헤더 디렉터리와 링크 객체 같은 정보는 원래 컴파일 단계에 직접 들어 있었지만, 이제는 root_module로 옮겨졌다. 따라서 compile.root_module.include_dirs와 compile.root_module.link_objects에서 읽어야 한다.
설치할 헤더는 원래 파일 하나의 형태만 있었지만, 이제는 파일과 디렉터리 두 종류로 나뉘어 각각 처리해야 한다.
CSourceFiles에 root 필드가 추가되어 파일들의 루트 디렉터리를 나타내게 되었다.
빌드 단계의 makeFn에서 진행 상태 노드의 타입이 포인터에서 값으로 바뀌었다.
마지막 변경은 조금 다르게 처리했다. @import("builtin").zig_version을 컴파일 타임에 확인하고, 버전에 따라 포인터와 값 중 하나를 선택했다.
버전 고정
Zig는 해시로 의존성을 고정하므로, README의 URL은 특정 커밋을 가리켜야 하고 해당 해시도 함께 있어야 한다. 새 커밋으로 올릴 때마다 URL과 해시를 모두 바꿔야 하는데, 수동으로 관리하기는 번거롭다. 그래서 master에 푸시할 때마다 최신 커밋을 가져와 zig fetch로 해시를 계산하고, README의 URL과 해시를 교체하는 GitHub Actions 워크플로를 만들었다. 덕분에 README의 의존성 URL과 해시를 그대로 복사해 사용할 수 있다.
Zig에 내장된 빌드 시스템은 C 프로젝트를 컴파일할 때도 쓸 수 있다. 다만 문제가 하나 있다. 에디터의 clangd가 각 파일에 어떤 헤더 경로와 컴파일 옵션을 써야 하는지 몰라서, 코드 곳곳에 빨간 줄이 생기고 정의로 이동하는 기능도 작동하지 않는다.
clangd는 프로젝트의 compile_commands.json을 참고한다. CMake는 이 파일을 바로 내보낼 수 있지만, zig build에는 이를 위한 기본 단계가 없다.
다행히 빌드 그래프에서 필요한 정보를 추출해 compile_commands.json을 생성하는 모듈을 이미 만든 사람이 있었다. 하지만 예전 Zig API를 기준으로 작성된 모듈이었고, 내가 쓰는 버전은 0.13이었다. 그사이 빌드 시스템 API가 많이 바뀌어서 그대로는 컴파일되지 않았다. 그래서 이 모듈을 바탕으로 수정한 버전을 만들었다. 저장소는 xxxbrian/zig-compile-commands에 있다.
사용법
먼저 build.zig.zon에 의존성으로 추가한다. 특정 커밋의 tarball URL과 그에 맞는 해시를 지정하면 된다. 아래의 <commit>과 <hash>는 실제 값으로 바꿔야 한다.
그다음 build.zig에서 모듈을 가져오고, 정보를 생성할 타깃을 리스트에 모은 뒤 마지막에 createStep을 호출한다. 아래 코드는 연결 방법을 보여 주는 예시다. C 소스 파일, 헤더 디렉터리, 링크할 라이브러리는 프로젝트에 맞게 추가한다.
이렇게 하면 zcc라는 단계가 추가된다. zig build zcc를 실행하면 프로젝트 루트에 compile_commands.json이 생성되고, clangd가 자동으로 찾아서 사용한다.
생성 원리
이 모듈은 빌드 그래프에서 C와 관련된 정보를 하나씩 clang 명령줄 인자로 바꾼다.
전달받은 각 타깃의 link_objects를 순회한다. C 소스 파일을 만나면 파일 경로와 해당 파일에 따로 지정한 컴파일 옵션을 기록한다. 시스템 라이브러리는 -l로 시작하는 인자로 바꾸고, libc나 libc++를 링크했다면 각각 -lc, -lc++도 추가한다. 헤더 디렉터리는 -I 인자로 바꾸며, 다른 타깃이 설치한 헤더의 디렉터리도 함께 넣는다. 또 타깃이 다른 타깃을 링크하고 있다면 그것도 리스트에 추가해 계속 순회한다. 따라서 최상위 실행 파일 타깃만 넘겨도 전체 정보를 모을 수 있다.
각 C 소스 파일은 최종적으로 compile_commands.json의 항목 하나가 된다. 여기에는 작업 디렉터리, 파일 경로, 출력 경로와 전체 인자 목록이 들어간다. clangd가 이해하는 것은 clang의 명령줄이므로, 인자 목록은 clang을 호출하는 형태로 작성한다.
createStep은 관련된 모든 타깃의 설정을 마친 뒤 호출해야 한다. 모듈이 호출 시점에 빌드 그래프를 읽기 때문에, 설정이 끝나지 않은 타깃이 있으면 해당 헤더 디렉터리와 소스 파일을 읽지 못한다.
한 가지 더 있다. 생성 단계가 실행될 때는 createStep을 호출하며 넘겼던 인자를 직접 가져올 수 없다. 그래서 모듈에서는 전역 변수에 타깃 리스트를 저장해 두고, 나중에 실행되는 단계가 읽을 수 있게 했다.
Zig 0.13에 맞추기
이전 API에서 0.13으로 바뀌면서 달라진 부분은 주로 다음과 같다.
마지막 변경은 조금 다르게 처리했다. @import("builtin").zig_version을 컴파일 타임에 확인하고, 버전에 따라 포인터와 값 중 하나를 선택했다.
버전 고정
Zig는 해시로 의존성을 고정하므로, README의 URL은 특정 커밋을 가리켜야 하고 해당 해시도 함께 있어야 한다. 새 커밋으로 올릴 때마다 URL과 해시를 모두 바꿔야 하는데, 수동으로 관리하기는 번거롭다. 그래서 master에 푸시할 때마다 최신 커밋을 가져와 zig fetch로 해시를 계산하고, README의 URL과 해시를 교체하는 GitHub Actions 워크플로를 만들었다. 덕분에 README의 의존성 URL과 해시를 그대로 복사해 사용할 수 있다.