Instructivo de Bazel: Compila un proyecto de Go

Informar un problema Ver código fuente Nocturno · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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.