En este instructivo, se presentan los conceptos básicos de Bazel y se muestra cómo compilar un proyecto de Go (Golang). Aprenderás a configurar tu espacio de trabajo, compilar un programa pequeño, importar una biblioteca y ejecutar su prueba. En el camino, aprenderás conceptos clave de Bazel, como los destinos y los archivos BUILD
.
Tiempo estimado para completar el codelab: 30 minutos
Antes de comenzar
Instala Bazel
Antes de comenzar, primero instala Bazel si aún no lo hiciste.
Para verificar si Bazel está instalado, ejecuta bazel version
en cualquier directorio.
Instala Go (opcional)
No es necesario que instales Go para compilar proyectos de Go con Bazel. El conjunto de reglas de Bazel Go descarga y usa automáticamente una cadena de herramientas de Go en lugar de usar la cadena de herramientas instalada en tu máquina. Esto garantiza que todos los desarrolladores de un proyecto compilen con la misma versión de Go.
Sin embargo, es posible que desees instalar una cadena de herramientas de Go para ejecutar comandos como go
get
y go mod tidy
.
Para verificar si Go está instalado, ejecuta go version
en cualquier directorio.
Obtén el proyecto de muestra
Los ejemplos de Bazel se almacenan en un repositorio de Git, por lo que deberás instalar Git si aún no lo hiciste. Para descargar el repositorio de ejemplos, ejecuta este comando:
git clone https://github.com/bazelbuild/examples
El proyecto de muestra para este instructivo se encuentra en el directorio examples/go-tutorial
.
Consulta su contenido:
go-tutorial/
└── stage1
└── stage2
└── stage3
Hay tres subdirectorios (stage1
, stage2
y stage3
), cada uno para una sección diferente de este instructivo. Cada etapa se basa en la anterior.
Compila con Bazel
Comenzaremos en el directorio stage1
, donde encontraremos un programa. Podemos compilarlo con bazel build
y, luego, ejecutarlo:
$ 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! 💚
También podemos compilar y ejecutar el programa con un solo 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! 💚
Comprende la estructura del proyecto
Echa un vistazo al proyecto que acabamos de compilar.
hello.go
contiene el código fuente de Go para el programa.
package main
import "fmt"
func main() {
fmt.Println("Hello, Bazel! 💚")
}
BUILD
contiene algunas instrucciones para Bazel, que le indican lo que queremos compilar.
Por lo general, escribirás un archivo como este en cada directorio. Para este proyecto, tenemos un solo destino go_binary
que compila nuestro programa desde hello.go
.
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "hello",
srcs = ["hello.go"],
)
MODULE.bazel
hace un seguimiento de las dependencias de tu proyecto. También marca el directorio raíz de tu proyecto, por lo que solo escribirás un archivo MODULE.bazel
por proyecto. Cumple un propósito similar al archivo go.mod
de Go. En realidad, no necesitas un archivo go.mod
en un proyecto de Bazel, pero puede ser útil tener uno para que puedas seguir usando go get
y go mod tidy
para la administración de dependencias. El conjunto de reglas de Bazel Go puede importar dependencias de go.mod
, pero lo abordaremos en otro instructivo.
Nuestro archivo MODULE.bazel
contiene una sola dependencia en rules_go, el conjunto de reglas de Go. Necesitamos esta dependencia porque Bazel no tiene compatibilidad integrada con Go.
bazel_dep(
name = "rules_go",
version = "0.50.1",
)
Por último, MODULE.bazel.lock
es un archivo generado por Bazel que contiene hashes y otros metadatos sobre nuestras dependencias. Incluye las dependencias implícitas que agrega Bazel, por lo que es bastante largo y no lo mostraremos aquí. Al igual que con go.sum
, debes confirmar tu archivo MODULE.bazel.lock
en el control de código fuente para asegurarte de que todos los miembros de tu proyecto obtengan la misma versión de cada dependencia. No deberías tener que editar MODULE.bazel.lock
manualmente.
Comprende el archivo BUILD
La mayor parte de tu interacción con Bazel se realizará a través de archivos BUILD
(o, de manera equivalente, archivos BUILD.bazel
), por lo que es importante que comprendas qué hacen.
Los archivos BUILD
se escriben en un lenguaje de programación llamado Starlark, un subconjunto limitado de Python.
Un archivo BUILD
contiene una lista de destinos. Un destino es algo que Bazel puede compilar, como un objeto binario, una biblioteca o una prueba.
Un destino llama a una función de regla con una lista de atributos para describir lo que se debe compilar. Nuestro ejemplo tiene dos atributos: name
identifica el destino en la línea de comandos y srcs
es una lista de rutas de acceso a archivos fuente (separadas por barras, relativas al directorio que contiene el archivo BUILD
).
Una regla le indica a Bazel cómo compilar un destino. En nuestro ejemplo, usamos la regla go_binary
. Cada regla define acciones (comandos) que generan un conjunto de archivos de salida. Por ejemplo, go_binary
define acciones de compilación y vinculación de Go que producen un archivo de salida ejecutable.
Bazel tiene reglas integradas para algunos lenguajes, como Java y C++. Puedes encontrar su documentación en la Enciclopedia de compilación. Puedes encontrar conjuntos de reglas para muchos otros lenguajes y herramientas en el Registro Central de Bazel (BCR).
Cómo agregar una biblioteca
Pasa al directorio stage2
, en el que compilaremos un programa nuevo que imprimirá tu fortuna. Este programa usa un paquete de Go independiente como una biblioteca que selecciona una fortuna de una lista predefinida de mensajes.
go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ └── fortune.go
└── print_fortune.go
fortune.go
es el archivo fuente de la biblioteca. La biblioteca fortune
es un paquete de Go independiente, por lo que sus archivos fuente se encuentran en un directorio separado. Bazel no requiere que mantengas los paquetes de Go en directorios separados, pero es una convención sólida en el ecosistema de Go, y seguirla te ayudará a mantener la compatibilidad con otras herramientas de 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))]
}
El directorio fortune
tiene su propio archivo BUILD
que le indica a Bazel cómo compilar este paquete. Aquí usamos go_library
en lugar de go_binary
.
También debemos establecer el atributo importpath
en una cadena con la que se pueda importar la biblioteca a otros archivos fuente de Go. Este nombre debe ser la ruta de acceso al repositorio (o la ruta de acceso al módulo) concatenada con el directorio dentro del repositorio.
Por último, debemos establecer el atributo visibility
en ["//visibility:public"]
.
visibility
se puede establecer en cualquier objetivo. Determina qué paquetes de Bazel pueden depender de este destino. En nuestro caso, queremos que cualquier destino pueda depender de esta biblioteca, por lo que usamos el 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"],
)
Puedes compilar esta biblioteca con el siguiente comando:
$ bazel build //fortune
A continuación, observa cómo print_fortune.go
usa este paquete.
package main
import (
"fmt"
"github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)
func main() {
fmt.Println(fortune.Get())
}
print_fortune.go
importa el paquete con la misma cadena declarada en el atributo importpath
de la biblioteca fortune
.
También debemos declarar esta dependencia en Bazel. Este es el archivo BUILD
en el directorio stage2
.
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "print_fortune",
srcs = ["print_fortune.go"],
deps = ["//fortune"],
)
Puedes ejecutarlo con el siguiente comando.
bazel run //:print_fortune
El destino print_fortune
tiene un atributo deps
, que es una lista de otros destinos de los que depende. Contiene "//fortune"
, una cadena de etiqueta que hace referencia al destino en el directorio fortune
llamado fortune
.
Bazel requiere que todos los destinos declaren sus dependencias de forma explícita con atributos como deps
. Esto puede parecer engorroso, ya que las dependencias también se especifican en los archivos fuente, pero la explicitud de Bazel le da una ventaja. Bazel compila un gráfico de acciones que contiene todos los comandos, las entradas y las salidas antes de ejecutar cualquier comando, sin leer ningún archivo fuente. Luego, Bazel puede almacenar en caché los resultados de las acciones o enviar acciones para la ejecución remota sin lógica específica del lenguaje integrada.
Cómo funcionan las etiquetas
Una etiqueta es una cadena que Bazel usa para identificar un destino o un archivo. Las etiquetas se usan en los argumentos de la línea de comandos y en los atributos de archivos BUILD
, como deps
. Ya vimos algunos, como //fortune
, //:print-fortune
y @rules_go//go:def.bzl
.
Una etiqueta tiene tres partes: un nombre de repositorio, un nombre de paquete y un nombre de destino (o archivo).
El nombre del repositorio se escribe entre @
y //
, y se usa para hacer referencia a un destino desde un módulo de Bazel diferente (por razones históricas, a veces se usan módulo y repositorio como sinónimos). En la etiqueta, @rules_go//go:def.bzl
, el nombre del repositorio es rules_go
. El nombre del repositorio se puede omitir cuando se hace referencia a destinos en el mismo repositorio.
El nombre del paquete se escribe entre //
y :
, y se usa para hacer referencia a un destino desde un paquete de Bazel diferente. En la etiqueta @rules_go//go:def.bzl
, el nombre del paquete es go
. Un paquete de Bazel es un conjunto de archivos y destinos definidos por un archivo BUILD
o BUILD.bazel
en su directorio de nivel superior.
El nombre del paquete es una ruta separada por barras desde el directorio raíz del módulo (que contiene MODULE.bazel
) hasta el directorio que contiene el archivo BUILD
. Un paquete puede incluir subdirectorios, pero solo si estos no contienen archivos BUILD
que definan sus propios paquetes.
La mayoría de los proyectos de Go tienen un archivo BUILD
por directorio y un paquete de Go por archivo BUILD
. El nombre del paquete en una etiqueta se puede omitir cuando se hace referencia a destinos en el mismo directorio.
El nombre del destino se escribe después de :
y hace referencia a un destino dentro de un paquete.
El nombre del destino se puede omitir si es el mismo que el último componente del nombre del paquete (por lo que //a/b/c:c
es igual a //a/b/c
; //fortune:fortune
es igual a //fortune
).
En la línea de comandos, puedes usar ...
como comodín para hacer referencia a todos los destinos dentro de un paquete. Esto es útil para compilar o probar todos los destinos en un repositorio.
# Build everything
$ bazel build //...
Prueba tu proyecto
A continuación, ve al directorio stage3
, donde agregaremos una prueba.
go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ ├── fortune.go
│ └── fortune_test.go
└── print-fortune.go
fortune/fortune_test.go
es nuestro nuevo archivo fuente de prueba.
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)
}
}
Este archivo usa la variable fortunes
no exportada, por lo que debe compilarse en el mismo paquete de Go que fortune.go
. Consulta el archivo BUILD
para ver cómo 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"],
)
Tenemos un nuevo destino fortune_test
que usa la regla go_test
para compilar y vincular un ejecutable de prueba. go_test
necesita compilar fortune.go
y fortune_test.go
juntos con el mismo comando, por lo que usamos el atributo embed
aquí para incorporar los atributos del destino fortune
en fortune_test
. embed
se usa con mayor frecuencia con go_test
y go_binary
, pero también funciona con go_library
, lo que a veces es útil para el código generado.
Tal vez te preguntes si el atributo embed
está relacionado con el paquete embed
de Go, que se usa para acceder a los archivos de datos copiados en un archivo ejecutable. Esta es una desafortunada colisión de nombres: el atributo embed
de rules_go se introdujo antes que el paquete embed
de Go. En cambio, rules_go usa embedsrcs
para enumerar los archivos que se pueden cargar con el paquete embed
.
Intenta ejecutar nuestra prueba con 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.
Puedes usar el comodín ...
para ejecutar todas las pruebas. Bazel también compilará destinos que no sean pruebas, por lo que puede detectar errores de compilación incluso en paquetes que no tienen pruebas.
$ bazel test //...
Conclusión y lecturas adicionales
En este instructivo, compilamos y probamos un pequeño proyecto de Go con Bazel, y aprendimos algunos conceptos básicos de Bazel en el proceso.
- Para comenzar a compilar otras aplicaciones con Bazel, consulta los instructivos de C++, Java, Android y iOS.
- También puedes consultar la lista de reglas recomendadas para otros idiomas.
- Para obtener más información sobre Go, consulta el módulo rules_go, en especial la documentación de las reglas principales de Go.
- Para obtener más información sobre cómo trabajar con módulos de Bazel fuera de tu proyecto, consulta dependencias externas. En particular, para obtener información sobre cómo depender de módulos y cadenas de herramientas de Go a través del sistema de módulos de Bazel, consulta Go with bzlmod.