Bazel 튜토리얼: Go 프로젝트 빌드

문제 신고 소스 보기 Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

이 튜토리얼에서는 Go (Golang) 프로젝트를 빌드하는 방법을 보여줌으로써 Bazel의 기본사항을 소개합니다. 작업공간을 설정하고, 소규모 프로그램을 빌드하고, 라이브러리를 가져오고, 테스트를 실행하는 방법을 알아봅니다. 그러면서 타겟 및 BUILD 파일과 같은 주요 Bazel 개념을 알아봅니다.

예상 완료 시간: 30분

시작하기 전에

Bazel 설치

아직 설치하지 않았다면 시작하기 전에 먼저 bazel을 설치합니다.

어떤 디렉터리에서든 bazel version를 실행하여 Bazel이 설치되어 있는지 확인할 수 있습니다.

Go 설치 (선택사항)

Bazel로 Go 프로젝트를 빌드하기 위해 Go를 설치할 필요는 없습니다. Bazel Go 규칙 집합은 머신에 설치된 도구 모음을 사용하는 대신 Go 도구 모음을 자동으로 다운로드하고 사용합니다. 이렇게 하면 프로젝트의 모든 개발자가 동일한 버전의 Go를 사용하여 빌드할 수 있습니다.

하지만 go getgo 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 getgo 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.gofortune 라이브러리의 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.gofortune_test.go를 함께 컴파일해야 하므로 여기서는 embed 속성을 사용하여 fortune 타겟의 속성을 fortune_test에 통합합니다. embed는 가장 일반적으로 go_testgo_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 개념을 알아봤습니다.

  • Bazel로 다른 애플리케이션을 빌드하려면 C++, Java, Android, iOS 튜토리얼을 참고하세요.
  • 다른 언어의 권장 규칙 목록을 확인할 수도 있습니다.
  • Go에 대한 자세한 내용은 rules_go 모듈, 특히 핵심 Go 규칙 문서를 참조하세요.
  • 프로젝트 외부에서 Bazel 모듈로 작업하는 방법에 대한 자세한 내용은 외부 종속 항목을 참조하세요. 특히 Bazel의 모듈 시스템을 통해 Go 모듈 및 도구 모음에 종속하는 방법에 관한 자세한 내용은 bzlmod를 사용한 Go를 참고하세요.