Este tutorial apresenta os conceitos básicos do Bazel mostrando como criar
um projeto Go (Golang). Você aprenderá a configurar seu espaço de trabalho, criar um pequeno programa, importar uma biblioteca e executar o teste dela. Ao longo do caminho, você aprenderá os principais
conceitos do Bazel, como destinos e arquivos BUILD
.
Tempo estimado de conclusão: 30 minutos
Antes de começar
Instalar o Bazel
Antes de começar, instale o Bazel, caso ainda não tenha feito.
Para verificar se o Bazel está instalado, execute bazel version
em qualquer diretório.
Instalar o Go (opcional)
Você não precisa instalar o Go para criar projetos Go com o Bazel. O conjunto de regras do Bazel Go faz o download automático e usa uma cadeia de ferramentas Go em vez da instalada na sua máquina. Isso garante que todos os desenvolvedores em um projeto sejam criados com a mesma versão do Go.
No entanto, talvez você ainda queira instalar uma cadeia de ferramentas do Go para executar comandos como go
get
e go mod tidy
.
Para verificar se o Go está instalado, execute go version
em qualquer diretório.
Acessar o projeto de amostra
Os exemplos do Bazel são armazenados em um repositório Git. Portanto, você precisará instalar o Git, caso ainda não tenha feito isso. Para fazer o download do repositório de exemplos, execute este comando:
git clone https://github.com/bazelbuild/examples
O projeto de exemplo deste tutorial está no diretório examples/go-tutorial
.
Confira o que ele contém:
go-tutorial/
└── stage1
└── stage2
└── stage3
Há três subdiretórios (stage1
, stage2
e stage3
), cada um para uma
seção diferente deste tutorial. Cada etapa é baseada na anterior.
Criação com o Bazel
Comece no diretório stage1
, onde vamos encontrar um programa. Podemos
criá-lo com bazel build
e, em seguida, executá-lo:
$ 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! 💚
Também podemos criar a execução do programa com um único comando 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! 💚
Noções básicas sobre a estrutura do projeto
Confira o projeto que acabamos de criar.
hello.go
contém o código-fonte Go do programa.
package main
import "fmt"
func main() {
fmt.Println("Hello, Bazel! 💚")
}
BUILD
contém algumas instruções para o Bazel, informando o que queremos criar.
Normalmente, você grava um arquivo como este em cada diretório. Para este projeto, temos um único destino go_binary
que cria nosso programa a partir de hello.go
.
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "hello",
srcs = ["hello.go"],
)
O MODULE.bazel
rastreia as dependências do projeto. Ele também marca o diretório raiz
do projeto. Portanto, grave apenas um arquivo MODULE.bazel
por projeto. Ele
serve a uma finalidade semelhante ao arquivo go.mod
do Go. Você não precisa de um arquivo
go.mod
em um projeto do Bazel, mas pode ser útil ter um para
continuar usando go get
e go mod tidy
para gerenciamento de dependências. O
conjunto de regras do Bazel Go pode importar dependências de go.mod
, mas vamos abordar isso em
outro tutorial.
Nosso arquivo MODULE.bazel
contém uma única dependência em rules_go, o conjunto de regras do Go. Precisamos
dessa dependência porque o Bazel não tem suporte integrado para Go.
bazel_dep(
name = "rules_go",
version = "0.50.1",
)
Por fim, MODULE.bazel.lock
é um arquivo gerado pelo Bazel que contém hashes
e outros metadados sobre nossas dependências. Ele inclui dependências implícitas
adicionadas pelo próprio Bazel, então é muito longo e não vamos mostrar aqui. Assim como
go.sum
, confirme o arquivo MODULE.bazel.lock
no controle de origem para
garantir que todos no projeto recebam a mesma versão de cada dependência. Não
é necessário editar MODULE.bazel.lock
manualmente.
Entender o arquivo BUILD
A maior parte da sua interação com o Bazel será por meio de arquivos BUILD
(ou,
equivalentemente, arquivos BUILD.bazel
). Por isso, é importante entender o que eles
fazem.
Os arquivos BUILD
são escritos em uma linguagem de script chamada
Starlark, um subconjunto limitado do Python.
Um arquivo BUILD
contém uma lista de
alvos. Um destino é algo
que o Bazel pode criar, como um binário, uma biblioteca ou um teste.
Um destino chama uma função de regra com uma lista de
atributos para descrever o que
precisa ser criado. Nosso exemplo tem dois atributos: name
identifica o destino na
linha de comando, e srcs
é uma lista de caminhos de arquivos de origem (separados por barra,
relativos ao diretório que contém o arquivo BUILD
).
Uma regra diz ao Bazel como criar um destino. No nosso exemplo, usamos a regra
go_binary
. Cada regra define ações (comandos) que geram um conjunto de arquivos de saída. Por exemplo, go_binary
define
ações de compilação e vinculação do Go que produzem um arquivo de saída executável.
O Bazel tem regras integradas para algumas linguagens, como Java e C++. A documentação está disponível na Encyclopedia do Build. Você pode encontrar conjuntos de regras para muitos outros idiomas e ferramentas no Registro Central do Bazel (BCR).
Adicionar uma biblioteca
Vá para o diretório stage2
, onde vamos criar um novo programa que
imprime sua fortuna. Esse programa usa um pacote Go separado como uma biblioteca que
seleciona uma previsão de uma lista predefinida de mensagens.
go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ └── fortune.go
└── print_fortune.go
fortune.go
é o arquivo de origem da biblioteca. A biblioteca fortune
é um pacote Go separado. Portanto, os arquivos de origem estão em um diretório separado. O Bazel
não exige que você mantenha pacotes Go em diretórios separados, mas é uma
convenção forte no ecossistema Go, e seguir essa regra vai ajudar você a manter
compatibilidade com outras ferramentas 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))]
}
O diretório fortune
tem o próprio arquivo BUILD
, que informa ao Bazel como criar
esse pacote. Usamos go_library
aqui em vez de go_binary
.
Também precisamos definir o atributo importpath
como uma string com a qual a
biblioteca pode ser importada para outros arquivos de origem do Go. Esse nome precisa ser o
caminho do repositório (ou caminho do módulo) concatenado com o diretório dentro do
repositório.
Por fim, precisamos definir o atributo visibility
como ["//visibility:public"]
.
visibility
pode ser definido em qualquer
destino. Ele determina quais pacotes do Bazel podem depender desse destino. No nosso
caso, queremos que qualquer destino possa depender dessa biblioteca. Por isso, usamos o
valor especial //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"],
)
É possível criar essa biblioteca com:
$ bazel build //fortune
Em seguida, confira como print_fortune.go
usa esse pacote.
package main
import (
"fmt"
"github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)
func main() {
fmt.Println(fortune.Get())
}
print_fortune.go
importa o pacote usando a mesma string declarada no atributo
importpath
da biblioteca fortune
.
Também precisamos declarar essa dependência para o Bazel. Este é o arquivo BUILD
no
diretório stage2
.
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "print_fortune",
srcs = ["print_fortune.go"],
deps = ["//fortune"],
)
Para isso, use o comando abaixo.
bazel run //:print_fortune
O destino print_fortune
tem um atributo deps
, uma lista de outros destinos
dos quais ele depende. Ele contém "//fortune"
, uma string de rótulo que se refere ao destino
no diretório fortune
chamado fortune
.
O Bazel exige que todos os destinos declarem as dependências explicitamente com
atributos como deps
. Isso pode parecer complicado, já que as dependências também
são especificadas nos arquivos de origem, mas a explicabilidade do Bazel oferece uma vantagem. O Bazel
cria um gráfico de ações
que contém todos os comandos, entradas e saídas antes de executar qualquer comando,
sem ler nenhum arquivo de origem. O Bazel pode armazenar em cache os resultados de ação ou enviar
ações para execução remota sem a lógica
integrada específica da linguagem.
Como compreender os rótulos
Um rótulo é uma string que o Bazel usa
para identificar um destino ou um arquivo. Os rótulos são usados em argumentos de linha de comando e em
atributos de arquivo BUILD
, como deps
. Já vimos alguns, como //fortune
,
//:print-fortune
e @rules_go//go:def.bzl
.
Um rótulo tem três partes: um nome de repositório, um nome de pacote e um nome de destino (ou arquivo).
O nome do repositório é escrito entre @
e //
e é usado para se referir a um
destino de um módulo Bazel diferente. Por motivos históricos, módulo e
repositório às vezes são usados como sinônimos. No rótulo,
@rules_go//go:def.bzl
, o nome do repositório é rules_go
. O nome do repositório
pode ser omitido ao se referir a destinos no mesmo repositório.
O nome do pacote é escrito entre //
e :
e é usado para se referir a um
destino de um pacote Bazel diferente. No rótulo @rules_go//go:def.bzl
,
o nome do pacote é go
. Um pacote
do Bazel é um conjunto de arquivos e
alvos definidos por um arquivo BUILD
ou BUILD.bazel
no diretório de nível superior.
O nome do pacote é um caminho separado por barra do diretório raiz do módulo
(que contém MODULE.bazel
) para o diretório que contém o arquivo BUILD
. Um
pacote pode incluir subdiretórios, mas somente se eles não contiverem arquivos BUILD
que definam os próprios pacotes.
A maioria dos projetos Go tem um arquivo BUILD
por diretório e um pacote Go por
arquivo BUILD
. O nome do pacote em um rótulo pode ser omitido ao se referir a
destinos no mesmo diretório.
O nome do destino é escrito após :
e se refere a um destino dentro de um pacote.
O nome do destino pode ser omitido se for igual ao último componente do
nome do pacote. Por exemplo, //a/b/c:c
é igual a //a/b/c
, //fortune:fortune
é
igual a //fortune
.
Na linha de comando, é possível usar ...
como um curinga para se referir a todos os destinos
em um pacote. Isso é útil para criar ou testar todos os destinos em um
repositório.
# Build everything
$ bazel build //...
Testar seu projeto
Em seguida, vá para o diretório stage3
, onde vamos adicionar um teste.
go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ ├── fortune.go
│ └── fortune_test.go
└── print-fortune.go
fortune/fortune_test.go
é o novo arquivo de origem de teste.
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)
}
}
Esse arquivo usa a variável fortunes
não exportada, então ele precisa ser compilado
no mesmo pacote Go que fortune.go
. Confira o arquivo BUILD
para saber
como isso funciona:
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"],
)
Temos um novo destino fortune_test
que usa a regra go_test
para compilar e
vincular um executável de teste. O go_test
precisa compilar fortune.go
e
fortune_test.go
com o mesmo comando. Portanto, usamos o atributo embed
aqui para incorporar os atributos do destino fortune
em
fortune_test
. embed
é mais comumente usado com go_test
e go_binary
,
mas também funciona com go_library
, que às vezes é útil para o código
gerado.
Talvez você esteja se perguntando se o atributo embed
está relacionado ao pacote
embed
do Go, que é usado para acessar arquivos de dados
copiados para um executável. Há uma colisão de nome: o atributo embed
de rules_go foi introduzido antes do pacote embed
do Go. Em vez disso, rules_go
usa o embedsrcs
para listar arquivos que podem ser carregados com o pacote embed
.
Tente executar nosso teste com 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.
É possível usar o curinga ...
para executar todos os testes. Ele também cria destinos
que não são testes, o que pode detectar erros de compilação mesmo em pacotes que não
têm testes.
$ bazel test //...
Conclusão e leitura complementar
Neste tutorial, criamos e testamos um pequeno projeto Go com o Bazel e aprendemos alguns conceitos principais do Bazel.
- Para começar a criar outros aplicativos com o Bazel, consulte os tutoriais para C++, Java, Android e iOS.
- Você também pode conferir a lista de regras recomendadas para outros idiomas.
- Para mais informações sobre o Go, consulte o módulo rules_go, em especial a documentação Regras principais do Go.
- Para saber mais sobre como trabalhar com módulos do Bazel fora do seu projeto, consulte dependências externas. Para saber como depender de módulos e toolchains do Go pelo sistema de módulo do Bazel, consulte Go com bzlmod.