이 튜토리얼에서는 Go (Golang) 프로젝트를 빌드하는 방법을 보여줌으로써 Bazel의 기본사항을 소개합니다. 작업공간을 설정하고, 소규모 프로그램을 빌드하고, 라이브러리를 가져오고, 테스트를 실행하는 방법을 알아봅니다. 그러면서 타겟 및 BUILD
파일과 같은 주요 Bazel 개념을 알아봅니다.
예상 완료 시간: 30분
시작하기 전에
Bazel 설치
아직 설치하지 않았다면 시작하기 전에 먼저 bazel을 설치합니다.
어떤 디렉터리에서든 bazel version
를 실행하여 Bazel이 설치되어 있는지 확인할 수 있습니다.
Go 설치 (선택사항)
Bazel로 Go 프로젝트를 빌드하기 위해 Go를 설치할 필요는 없습니다. Bazel Go 규칙 집합은 머신에 설치된 도구 모음을 사용하는 대신 Go 도구 모음을 자동으로 다운로드하고 사용합니다. 이렇게 하면 프로젝트의 모든 개발자가 동일한 버전의 Go를 사용하여 빌드할 수 있습니다.
하지만 go
get
및 go mod tidy
와 같은 명령어를 실행하려면 Go 도구 모음을 설치하는 것이 좋습니다.
디렉터리에서 go version
를 실행하여 Go가 설치되어 있는지 확인할 수 있습니다.
샘플 프로젝트 가져오기
Bazel 예시는 Git 저장소에 저장되므로 아직 설치하지 않았다면 Git를 설치해야 합니다. 예시 저장소를 다운로드하려면 다음 명령어를 실행합니다.
git clone https://github.com/bazelbuild/examples
이 튜토리얼의 샘플 프로젝트는 examples/go-tutorial
디렉터리에 있습니다.
포함된 내용은 다음과 같습니다.
go-tutorial/
└── stage1
└── stage2
└── stage3
이 튜토리얼의 각 섹션에 해당하는 세 개의 하위 디렉터리 (stage1
, stage2
, stage3
)가 있습니다. 각 단계는 이전 단계를 기반으로 합니다.
Bazel을 사용하여 빌드
프로그램이 있는 stage1
디렉터리에서 시작합니다. bazel build
로 빌드한 다음 실행할 수 있습니다.
$ cd go-tutorial/stage1/
$ bazel build //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
bazel-bin/hello_/hello
INFO: Elapsed time: 0.473s, Critical Path: 0.25s
INFO: 3 processes: 1 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 3 total actions
$ bazel-bin/hello_/hello
Hello, Bazel! 💚
단일 bazel run
명령어로 프로그램을 빌드하고 실행할 수도 있습니다.
$ bazel run //:hello
bazel run //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
bazel-bin/hello_/hello
INFO: Elapsed time: 0.128s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/hello_/hello
Hello, Bazel! 💚
프로젝트 구조 이해
방금 빌드한 프로젝트를 살펴보세요.
hello.go
에는 프로그램의 Go 소스 코드가 포함되어 있습니다.
package main
import "fmt"
func main() {
fmt.Println("Hello, Bazel! 💚")
}
BUILD
에는 Bazel에 빌드할 항목을 알려주는 몇 가지 안내가 포함되어 있습니다.
일반적으로 각 디렉터리에 이와 같은 파일을 작성합니다. 이 프로젝트에는 hello.go
에서 프로그램을 빌드하는 단일 go_binary
타겟이 있습니다.
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "hello",
srcs = ["hello.go"],
)
MODULE.bazel
는 프로젝트의 종속 항목을 추적합니다. 또한 프로젝트의 루트 디렉터리를 표시하므로 프로젝트당 MODULE.bazel
파일을 하나만 작성하면 됩니다. Go의 go.mod
파일과 유사한 용도로 사용됩니다. Bazel 프로젝트에는 실제로 go.mod
파일이 필요하지 않지만 종속 항목 관리에 go get
및 go mod tidy
를 계속 사용할 수 있도록 go.mod
파일을 사용하는 것이 유용할 수 있습니다. Bazel Go 규칙 세트는 go.mod
에서 종속 항목을 가져올 수 있지만 이는 다른 튜토리얼에서 다룹니다.
MODULE.bazel
파일에는 Go 규칙 세트인 rules_go에 대한 단일 종속 항목이 포함되어 있습니다. Bazel에는 Go에 대한 기본 제공 지원이 없으므로 이 종속 항목이 필요합니다.
bazel_dep(
name = "rules_go",
version = "0.50.1",
)
마지막으로 MODULE.bazel.lock
는 종속 항목에 관한 해시 및 기타 메타데이터가 포함된 Bazel에서 생성된 파일입니다. Bazel 자체에서 추가한 암시적 종속 항목이 포함되어 있으므로 상당히 길며 여기에 표시되지 않습니다. go.sum
와 마찬가지로 프로젝트의 모든 사용자가 각 종속 항목의 동일한 버전을 가져올 수 있도록 MODULE.bazel.lock
파일을 소스 제어에 커밋해야 합니다. MODULE.bazel.lock
는 수동으로 수정할 필요가 없습니다.
BUILD 파일 이해하기
Bazel과의 대부분의 상호작용은 BUILD
파일 (또는 이에 상응하는 BUILD.bazel
파일)을 통해 이루어지므로, 각자의 역할을 이해하는 것이 중요합니다.
BUILD
파일은 Python의 제한된 하위 집합인 Starlark라는 스크립트 언어로 작성됩니다.
BUILD
파일에는 타겟 목록이 포함됩니다. 대상은 바이너리, 라이브러리, 테스트와 같이 Bazel이 빌드할 수 있는 항목입니다.
타겟은 빌드할 항목을 설명하는 속성 목록이 포함된 규칙 함수를 호출합니다. 이 예에는 두 가지 속성이 있습니다. name
는 명령줄에서 타겟을 식별하고 srcs
는 소스 파일 경로 목록입니다 (BUILD
파일이 포함된 디렉터리를 기준으로 슬래시로 구분됨).
규칙은 Bazel에 타겟을 빌드하는 방법을 알려줍니다. 이 예에서는 go_binary
규칙을 사용했습니다. 각 규칙은 출력 파일 세트를 생성하는 작업(명령어)을 정의합니다. 예를 들어 go_binary
는 실행 가능한 출력 파일을 생성하는 Go 컴파일 및 링크 작업을 정의합니다.
Bazel에는 Java 및 C++와 같은 몇 가지 언어에 적용되는 기본 제공 규칙이 있습니다. 빌드 백과사전의 문서를 참고하세요. Bazel Central Registry(BCR)에서 다른 많은 언어와 도구의 규칙 세트를 확인할 수 있습니다.
라이브러리 추가
stage2
디렉터리로 이동하여 운세를 출력하는 새 프로그램을 빌드합니다. 이 프로그램은 사전 정의된 메시지 목록에서 운세를 선택하는 라이브러리로 별도의 Go 패키지를 사용합니다.
go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ └── fortune.go
└── print_fortune.go
fortune.go
는 라이브러리의 소스 파일입니다. fortune
라이브러리는 별도의 Go 패키지이므로 소스 파일이 별도의 디렉터리에 있습니다. Bazel은 Go 패키지를 별도의 디렉터리에 보관하도록 요구하지 않지만 Go 생태계에서는 강력한 관례이며 이를 따르면 다른 Go 도구와 호환되는 데 도움이 됩니다.
package fortune
import "math/rand"
var fortunes = []string{
"Your build will complete quickly.",
"Your dependencies will be free of bugs.",
"Your tests will pass.",
}
func Get() string {
return fortunes[rand.Intn(len(fortunes))]
}
fortune
디렉터리에는 Bazel에 이 패키지를 빌드하는 방법을 알려주는 자체 BUILD
파일이 있습니다. 여기서는 go_binary
대신 go_library
를 사용합니다.
또한 importpath
속성을 라이브러리를 다른 Go 소스 파일로 가져올 수 있는 문자열로 설정해야 합니다. 이 이름은 저장소 내의 디렉터리와 연결된 저장소 경로 (또는 모듈 경로)여야 합니다.
마지막으로 visibility
속성을 ["//visibility:public"]
로 설정해야 합니다.
visibility
는 모든 타겟에 설정할 수 있습니다. 이 타겟에 종속될 수 있는 Bazel 패키지를 결정합니다. 여기서는 모든 타겟이 이 라이브러리에 종속될 수 있도록 하려고 하므로 특수 값 //visibility:public
를 사용합니다.
load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "fortune",
srcs = ["fortune.go"],
importpath = "github.com/bazelbuild/examples/go-tutorial/stage2/fortune",
visibility = ["//visibility:public"],
)
다음을 사용하여 이 라이브러리를 빌드할 수 있습니다.
$ bazel build //fortune
다음으로 print_fortune.go
에서 이 패키지를 사용하는 방법을 살펴봅니다.
package main
import (
"fmt"
"github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)
func main() {
fmt.Println(fortune.Get())
}
print_fortune.go
는 fortune
라이브러리의 importpath
속성에 선언된 것과 동일한 문자열을 사용하여 패키지를 가져옵니다.
또한 이 종속 항목을 Bazel에 선언해야 합니다. 다음은 stage2
디렉터리에 있는 BUILD
파일입니다.
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "print_fortune",
srcs = ["print_fortune.go"],
deps = ["//fortune"],
)
아래 명령어를 사용하여 실행할 수 있습니다.
bazel run //:print_fortune
print_fortune
타겟에는 종속되는 다른 타겟 목록인 deps
속성이 있습니다. 이 파일에는 fortune
라는 fortune
디렉터리의 타겟을 참조하는 라벨 문자열인 "//fortune"
가 포함됩니다.
Bazel을 사용하려면 모든 대상이 deps
과 같은 속성을 사용하여 종속 항목을 명시적으로 선언해야 합니다. 종속 항목은 소스 파일에서도 지정되므로 번거로울 수 있지만 Bazel의 명시성은 이점을 제공합니다. Bazel은 소스 파일을 읽지 않고 명령어를 실행하기 전에 모든 명령어, 입력, 출력을 포함하는 작업 그래프를 빌드합니다. 그러면 Bazel은 내장된 언어별 로직 없이 작업 결과를 캐시하거나 원격 실행을 위해 작업을 전송할 수 있습니다.
라벨 이해하기
라벨은 Bazel에서 타겟이나 파일을 식별하는 데 사용하는 문자열입니다. 라벨은 명령줄 인수와 BUILD
파일 속성(예: deps
)에서 사용됩니다. //fortune
, //:print-fortune
, @rules_go//go:def.bzl
와 같은 몇 가지는 이미 살펴봤습니다.
라벨은 저장소 이름, 패키지 이름, 타겟 (또는 파일) 이름의 세 부분으로 구성됩니다.
저장소 이름은 @
와 //
사이에 쓰이며 다른 Bazel 모듈의 타겟을 참조하는 데 사용됩니다. 이전의 이유로 모듈과 저장소가 동의어로 사용되는 경우가 있습니다. 라벨 @rules_go//go:def.bzl
의 저장소 이름은 rules_go
입니다. 동일한 저장소의 대상을 참조하는 경우 저장소 이름을 생략할 수 있습니다.
패키지 이름은 //
와 :
사이에 쓰이며 다른 Bazel 패키지의 타겟을 참조하는 데 사용됩니다. @rules_go//go:def.bzl
라벨에서 패키지 이름은 go
입니다. Bazel 패키지는 최상위 디렉터리의 BUILD
또는 BUILD.bazel
파일로 정의된 파일 및 타겟 집합입니다.
패키지 이름은 모듈 루트 디렉터리(MODULE.bazel
포함)에서 BUILD
파일이 포함된 디렉터리로 연결되는 슬래시로 구분된 경로입니다. 패키지에 하위 디렉터리가 포함될 수 있지만 자체 패키지를 정의하는 BUILD
파일도 포함되지 않은 경우에만 하위 디렉터리가 포함될 수 있습니다.
대부분의 Go 프로젝트에는 디렉터리당 BUILD
파일 하나와 BUILD
파일당 Go 패키지 하나가 있습니다. 동일한 디렉터리의 타겟을 참조할 때 라벨의 패키지 이름은 생략할 수 있습니다.
타겟 이름은 :
뒤에 쓰이며 패키지 내의 타겟을 참조합니다.
대상 이름이 패키지 이름의 마지막 구성요소와 동일한 경우 생략할 수 있습니다 (따라서 //a/b/c:c
는 //a/b/c
와 동일하고 //fortune:fortune
는 //fortune
와 동일함).
명령줄에서 ...
를 와일드 카드로 사용하여 패키지 내의 모든 대상을 참조할 수 있습니다. 이는 저장소의 모든 타겟을 빌드하거나 테스트하는 데 유용합니다.
# Build everything
$ bazel build //...
프로젝트 테스트
다음으로 테스트를 추가할 stage3
디렉터리로 이동합니다.
go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ ├── fortune.go
│ └── fortune_test.go
└── print-fortune.go
fortune/fortune_test.go
는 새 테스트 소스 파일입니다.
package fortune
import (
"slices"
"testing"
)
// TestGet checks that Get returns one of the strings from fortunes.
func TestGet(t *testing.T) {
msg := Get()
if i := slices.Index(fortunes, msg); i < 0 {
t.Errorf("Get returned %q, not one the expected messages", msg)
}
}
이 파일은 내보내지 않은 fortunes
변수를 사용하므로 fortune.go
와 동일한 Go 패키지로 컴파일해야 합니다. BUILD
파일을 살펴보고 작동 방식을 확인합니다.
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "fortune",
srcs = ["fortune.go"],
importpath = "github.com/bazelbuild/examples/go-tutorial/stage3/fortune",
visibility = ["//visibility:public"],
)
go_test(
name = "fortune_test",
srcs = ["fortune_test.go"],
embed = [":fortune"],
)
go_test
규칙을 사용하여 테스트 실행 파일을 컴파일하고 연결하는 새 fortune_test
타겟이 있습니다. go_test
는 동일한 명령어로 fortune.go
와 fortune_test.go
를 함께 컴파일해야 하므로 여기서는 embed
속성을 사용하여 fortune
타겟의 속성을 fortune_test
에 통합합니다. embed
는 가장 일반적으로 go_test
및 go_binary
와 함께 사용되지만 생성된 코드에 유용한 경우 go_library
와도 작동합니다.
embed
속성이 실행 파일에 복사된 데이터 파일에 액세스하는 데 사용되는 Go의 embed
패키지와 관련이 있는지 궁금할 수 있습니다. 불행한 이름 충돌입니다. rules_go의 embed
속성이 Go의 embed
패키지보다 먼저 도입되었습니다. 대신 rules_go는 embedsrcs
를 사용하여 embed
패키지로 로드할 수 있는 파일을 나열합니다.
bazel test
로 테스트를 실행해 봅니다.
$ bazel test //fortune:fortune_test
INFO: Analyzed target //fortune:fortune_test (0 packages loaded, 0 targets configured).
INFO: Found 1 test target...
Target //fortune:fortune_test up-to-date:
bazel-bin/fortune/fortune_test_/fortune_test
INFO: Elapsed time: 0.168s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
//fortune:fortune_test PASSED in 0.3s
Executed 0 out of 1 test: 1 test passes.
There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.
...
와일드 카드를 사용하여 모든 테스트를 실행할 수 있습니다. Bazel은 테스트가 아닌 타겟도 빌드하므로 테스트가 없는 패키지에서도 컴파일 오류를 포착할 수 있습니다.
$ bazel test //...
결론 및 추가 읽기 자료
이 튜토리얼에서는 Bazel을 사용하여 소규모 Go 프로젝트를 빌드하고 테스트했으며, 그 과정에서 몇 가지 핵심 Bazel 개념을 알아봤습니다.