Instructivo de Bazel: Compila un proyecto de Go

Denuncia un problema Ver fuente Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

En este instructivo, se presentan los conceptos básicos de Bazel y se muestra cómo compilar un proyecto Go (Golang). Aprenderás a configurar tu lugar de trabajo, compilar un programa pequeño, importar una biblioteca y ejecutar su prueba. En el camino, aprenderás los conceptos clave de Bazel, como los destinos y los archivos BUILD.

Tiempo estimado de finalización: 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 aún quieras 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 ejemplo

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 ejemplo de este instructivo se encuentra en el directorio examples/go-tutorial. Mira qué contiene:

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

Comienza 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

Mira el proyecto que acabamos de crear.

hello.go contiene el código fuente de Go del 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 realiza 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. Su uso es 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 desde go.mod, pero lo veremos 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 que genera Bazel y contiene hashes y otros metadatos sobre nuestras dependencias. Incluye dependencias implícitas que agregó Bazel, por lo que es bastante largo, por lo que 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 necesitar editar MODULE.bazel.lock de forma manual.

Comprende el archivo BUILD

La mayor parte de tu interacción con Bazel se realizará mediante archivos BUILD (o, equivalentemente, archivos BUILD.bazel), por lo que es importante comprender lo que 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 objetivos. Un destino es algo que Bazel puede compilar, como un objeto binario, una biblioteca o una prueba.

Un objetivo 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 de archivos de origen (separadas por barras, en relación con el directorio que contiene el archivo BUILD).

Una regla le indica a Bazel cómo compilar un objetivo. 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 las 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 compilaciones. Puedes encontrar conjuntos de reglas para muchos otros lenguajes y herramientas en el Registro central de Bazel (BCR).

Cómo agregar una biblioteca

Ve al directorio stage2, en el que compilaremos un nuevo programa que imprima tu destino. Este programa usa un paquete Go independiente como una biblioteca que selecciona una adivinanza 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 Go independiente, por lo que sus archivos fuente se encuentran en un directorio independiente. 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 del repositorio (o la ruta de acceso del 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 lo siguiente:

$ 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 objetivo print_fortune tiene un atributo deps, una lista de otros objetivos 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 claridad de Bazel le da una ventaja. Bazel compila un grafo de acciones que contiene todos los comandos, entradas y salidas antes de ejecutarlos, sin leer ningún archivo fuente. Luego, Bazel puede almacenar en caché los resultados de acciones o enviar acciones para la ejecución remota sin una 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 del archivo 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 objetivo de un módulo de Bazel diferente (por razones históricas, módulo y repositorio a veces se usan como sinónimos). En la etiqueta, @rules_go//go:def.bzl, el nombre del repositorio es rules_go. Se puede omitir el nombre del repositorio 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 objetivo de un paquete 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. Su nombre de paquete es una ruta de acceso separada por barras del directorio raíz del módulo (que contiene MODULE.bazel) al directorio que contiene el archivo BUILD. Un paquete puede incluir subdirectorios, pero solo si 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. Se puede omitir el nombre del paquete en una etiqueta cuando se hace referencia a objetivos en el mismo directorio.

El nombre del destino se escribe después de : y hace referencia a un destino dentro de un paquete. Se puede omitir el nombre de destino 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 de 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 debe compilar fortune.go y fortune_test.go junto con el mismo comando, por lo que usamos el atributo embed aquí para incorporar los atributos del destino fortune en fortune_test. Por lo general, embed se usa con go_test y go_binary, pero también funciona con go_library, que a veces es útil para el código generado.

Es posible que te preguntes si el atributo embed está relacionado con el paquete embed de Go, que se usa para acceder a archivos de datos copiados en un ejecutable. Esta es una colisión de nombres desafortunada: el atributo embed de rules_go se introdujo antes que el paquete embed de Go. En su lugar, rules_go usa embedsrcs para enumerar los archivos que se pueden cargar con el paquete embed.

Intenta ejecutar la 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 son 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 durante el proceso.

  • Para comenzar a compilar otras aplicaciones con Bazel, consulta los instructivos para 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, si deseas obtener información para depender de las cadenas de herramientas y los módulos de Go a través del sistema de módulos de Bazel, consulta Go con bzlmod.