Instructivo de Bazel: Compila un proyecto de Java

Informar un problema Ver fuente

En este instructivo, se abordan los conceptos básicos de la compilación de aplicaciones de Java con Bazel. Configurarás tu lugar de trabajo y compilarás un proyecto de Java simple que ilustre conceptos clave de Bazel, como destinos y archivos BUILD.

Tiempo estimado de finalización: 30 minutos.

Qué aprenderás

En este instructivo, aprenderás a hacer lo siguiente:

  • Crear un objetivo
  • Visualizar las dependencias del proyecto
  • Dividir el proyecto en varios destinos y paquetes
  • Controla la visibilidad de los objetivos en los paquetes
  • Haz referencia a destinos mediante etiquetas
  • Implementa un destino

Antes de comenzar

Instala Bazel

Para prepararte para el instructivo, primero instala Bazel si aún no lo tienes.

Cómo instalar el JDK

  1. Instala Java JDK (la versión preferida es la 11; sin embargo, se admiten las versiones entre la 8 y la 15).

  2. Configura la variable de entorno JAVA_HOME para que apunte al JDK.

    • En Linux o macOS:

      export JAVA_HOME="$(dirname $(dirname $(realpath $(which javac))))"
      
    • En Windows:

      1. Abre el Panel de control.
      2. Ve a "Sistema y seguridad" > "Sistema" > "Configuración avanzada del sistema" > pestaña "Avanzada" > "Variables de entorno..." .
      3. En la lista "Variables de usuario" (la que aparece en la parte superior), haz clic en "Nueva...".
      4. En el campo "Nombre de la variable", ingresa JAVA_HOME.
      5. Haz clic en "Explorar directorio...".
      6. Navega al directorio de JDK (por ejemplo, C:\Program Files\Java\jdk1.8.0_152).
      7. Haz clic en "Aceptar" en todas las ventanas de diálogo.

Obtén el proyecto de muestra

Recupera el proyecto de muestra del repositorio de GitHub de Bazel:

git clone https://github.com/bazelbuild/examples

El proyecto de muestra para este instructivo se encuentra en el directorio examples/java-tutorial y se estructura de la siguiente manera:

java-tutorial
├── BUILD
├── src
│   └── main
│       └── java
│           └── com
│               └── example
│                   ├── cmdline
│                   │   ├── BUILD
│                   │   └── Runner.java
│                   ├── Greeting.java
│                   └── ProjectRunner.java
└── MODULE.bazel

Compila con Bazel

Cómo configurar el espacio de trabajo

Antes de compilar un proyecto, debes configurar su lugar de trabajo. Un lugar de trabajo es un directorio que contiene los archivos de origen de tu proyecto y los resultados de compilación de Bazel. También contiene archivos que Bazel reconoce como especiales:

  • El archivo MODULE.bazel, que identifica el directorio y su contenido como un lugar de trabajo de Bazel y se encuentra en la raíz de la estructura de directorios del proyecto

  • Uno o más archivos BUILD, que le indican a Bazel cómo compilar diferentes partes del proyecto. Un directorio dentro del lugar de trabajo que contiene un archivo BUILD es un paquete. Más adelante en este instructivo, aprenderás sobre los paquetes).

Para designar un directorio como un lugar de trabajo de Bazel, crea un archivo vacío llamado MODULE.bazel en ese directorio.

Cuando Bazel compila el proyecto, todas las entradas y dependencias deben estar en el mismo lugar de trabajo. Los archivos que residen en lugares de trabajo diferentes son independientes entre sí, a menos que estén vinculados, lo que está fuera del alcance de este instructivo.

Comprende el archivo BUILD

Un archivo BUILD contiene varios tipos diferentes de instrucciones para Bazel. El tipo más importante es la regla de compilación, que le indica a Bazel cómo compilar los resultados deseados, como bibliotecas o objetos binarios ejecutables. Cada instancia de una regla de compilación en el archivo BUILD se denomina destino y apunta a un conjunto específico de archivos de origen y dependencias. Un objetivo también puede apuntar a otros objetivos.

Observa el archivo java-tutorial/BUILD:

java_binary(
    name = "ProjectRunner",
    srcs = glob(["src/main/java/com/example/*.java"]),
)

En nuestro ejemplo, el destino ProjectRunner crea una instancia de la regla java_binary integrada de Bazel. La regla le indica a Bazel que compile un archivo .jar y una secuencia de comandos de shell del wrapper (ambas nombran según el destino).

Los atributos en el destino indican explícitamente sus dependencias y opciones. Si bien el atributo name es obligatorio, muchos son opcionales. Por ejemplo, en el destino de la regla ProjectRunner, name es el nombre del destino, srcs especifica los archivos de origen que Bazel usa para compilar el destino y main_class especifica la clase que contiene el método principal. (Tal vez hayas notado que en nuestro ejemplo se usa glob para pasar un conjunto de archivos de origen a Bazel en lugar de enumerarlos uno por uno).

Compila el proyecto

Para compilar tu proyecto de muestra, navega al directorio java-tutorial y ejecuta lo siguiente:

bazel build //:ProjectRunner

En la etiqueta de destino, la parte // es la ubicación del archivo BUILD relativa a la raíz del lugar de trabajo (en este caso, la raíz misma) y ProjectRunner es el nombre del destino en el archivo BUILD. Al final de este instructivo, obtendrás más información sobre las etiquetas de destino.

Bazel produce un resultado similar al siguiente:

   INFO: Found 1 target...
   Target //:ProjectRunner up-to-date:
      bazel-bin/ProjectRunner.jar
      bazel-bin/ProjectRunner
   INFO: Elapsed time: 1.021s, Critical Path: 0.83s

¡Felicitaciones! Acabas de compilar tu primer destino de Bazel. Bazel coloca los resultados de compilación en el directorio bazel-bin, en la raíz del lugar de trabajo. Explora el contenido para tener una idea de la estructura de resultados de Bazel.

Ahora prueba tu objeto binario recién compilado:

bazel-bin/ProjectRunner

Cómo revisar el gráfico de dependencias

Bazel requiere que las dependencias de compilación se declaren de forma explícita en los archivos BUILD. Bazel usa esas declaraciones para crear el gráfico de dependencia del proyecto, lo que permite compilaciones incrementales precisas.

Para visualizar las dependencias del proyecto de muestra, puedes generar una representación de texto del gráfico de dependencias mediante la ejecución de este comando en la raíz del lugar de trabajo:

bazel query  --notool_deps --noimplicit_deps "deps(//:ProjectRunner)" --output graph

El comando anterior le indica a Bazel que busque todas las dependencias del //:ProjectRunner de destino (sin incluir las dependencias implícitas y de host) y que formatee el resultado como un gráfico.

Luego, pega el texto en GraphViz.

Como puedes ver, el proyecto tiene un solo destino que compila dos archivos de origen sin dependencias adicionales:

Gráfico de dependencias del “ProjectRunner” de destino

Después de configurar tu lugar de trabajo, compilar tu proyecto y examinar sus dependencias, puedes agregar algo de complejidad.

Define mejor tu compilación de Bazel

Si bien un solo destino es suficiente para los proyectos pequeños, es posible que quieras dividir los proyectos más grandes en varios destinos y paquetes para permitir compilaciones incrementales rápidas (es decir, solo volver a compilar lo que cambió) y acelerar tus compilaciones compilando varias partes de un proyecto a la vez.

Cómo especificar varios destinos de compilación

Puedes dividir la compilación del proyecto de muestra en dos destinos. Reemplaza el contenido del archivo java-tutorial/BUILD con lo siguiente:

java_binary(
    name = "ProjectRunner",
    srcs = ["src/main/java/com/example/ProjectRunner.java"],
    main_class = "com.example.ProjectRunner",
    deps = [":greeter"],
)

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
)

Con esta configuración, Bazel primero compila la biblioteca greeter y, luego, el objeto binario ProjectRunner. El atributo deps en java_binary le indica a Bazel que se requiere la biblioteca greeter para compilar el objeto binario ProjectRunner.

Para compilar esta nueva versión del proyecto, ejecuta el siguiente comando:

bazel build //:ProjectRunner

Bazel produce un resultado similar al siguiente:

INFO: Found 1 target...
Target //:ProjectRunner up-to-date:
  bazel-bin/ProjectRunner.jar
  bazel-bin/ProjectRunner
INFO: Elapsed time: 2.454s, Critical Path: 1.58s

Ahora prueba tu objeto binario recién compilado:

bazel-bin/ProjectRunner

Si ahora modificas ProjectRunner.java y vuelves a compilar el proyecto, Bazel solo vuelve a compilar ese archivo.

Si observas el gráfico de dependencias, puedes ver que ProjectRunner depende de las mismas entradas que antes, pero la estructura de la compilación es diferente:

Gráfico de dependencias del "ProjectRunner" de destino después de agregar una dependencia

Ya compilaste el proyecto con dos objetivos. El destino ProjectRunner compila dos archivos de origen y depende de otro destino (:greeter), que compila un archivo de origen adicional.

Cómo usar varios paquetes

Ahora vamos a dividir el proyecto en varios paquetes. Si observas el directorio src/main/java/com/example/cmdline, puedes ver que también contiene un archivo BUILD, además de algunos archivos fuente. Por lo tanto, para Bazel, el lugar de trabajo ahora contiene dos paquetes, //src/main/java/com/example/cmdline y // (ya que hay un archivo BUILD en la raíz del lugar de trabajo).

Observa el archivo src/main/java/com/example/cmdline/BUILD:

java_binary(
    name = "runner",
    srcs = ["Runner.java"],
    main_class = "com.example.cmdline.Runner",
    deps = ["//:greeter"],
)

El destino runner depende del objetivo greeter en el paquete // (por lo tanto, la etiqueta de destino //:greeter). Bazel lo sabe a través del atributo deps. Observa el gráfico de dependencia:

Gráfico de dependencias del "ejecutor" objetivo

Sin embargo, para que la compilación tenga éxito, debes otorgar de forma explícita el objetivo runner en la visibilidad //src/main/java/com/example/cmdline/BUILD a los objetivos en //BUILD mediante el atributo visibility. Esto se debe a que, de forma predeterminada, los destinos solo son visibles para otros destinos del mismo archivo BUILD. (Bazel usa la visibilidad de destino para evitar problemas, como bibliotecas con detalles de implementación que se filtren a las APIs públicas).

Para ello, agrega el atributo visibility al destino greeter en java-tutorial/BUILD, como se muestra a continuación:

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
    visibility = ["//src/main/java/com/example/cmdline:__pkg__"],
)

Ahora puedes compilar el paquete nuevo ejecutando el siguiente comando en la raíz del lugar de trabajo:

bazel build //src/main/java/com/example/cmdline:runner

Bazel produce un resultado similar al siguiente:

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner.jar
  bazel-bin/src/main/java/com/example/cmdline/runner
  INFO: Elapsed time: 1.576s, Critical Path: 0.81s

Ahora prueba tu objeto binario recién compilado:

./bazel-bin/src/main/java/com/example/cmdline/runner

Ahora modificaste el proyecto para compilarlo como dos paquetes, cada uno con un destino, y comprendes las dependencias entre ellos.

Usar etiquetas como referencia a los destinos

En los archivos BUILD y en la línea de comandos, Bazel usa etiquetas de destino para hacer referencia a los destinos, por ejemplo, //:ProjectRunner o //src/main/java/com/example/cmdline:runner. Su sintaxis es la siguiente:

//path/to/package:target-name

Si el destino es un destino de la regla, path/to/package es la ruta de acceso al directorio que contiene el archivo BUILD, y target-name es el nombre que le asignaste en el archivo BUILD (el atributo name). Si el destino es un archivo de destino, path/to/package es la ruta de acceso a la raíz del paquete, y target-name es el nombre del archivo de destino, incluida su ruta de acceso completa.

Cuando hagas referencia a destinos en la raíz del repositorio, la ruta del paquete está vacía, solo usa //:target-name. Cuando hagas referencia a destinos dentro del mismo archivo BUILD, puedes omitir el identificador raíz del lugar de trabajo // y solo usar :target-name.

Por ejemplo, para los destinos del archivo java-tutorial/BUILD, no tuviste que especificar una ruta de paquete, ya que la raíz del lugar de trabajo es un paquete (//) y las dos etiquetas de destino eran simplemente //:ProjectRunner y //:greeter.

Sin embargo, para los objetivos del archivo //src/main/java/com/example/cmdline/BUILD, debías especificar la ruta completa del paquete de //src/main/java/com/example/cmdline y la etiqueta de destino era //src/main/java/com/example/cmdline:runner.

Empaqueta un destino de Java para la implementación

Ahora, empaquetemos un objetivo de Java para su implementación mediante la compilación del objeto binario con todas sus dependencias del entorno de ejecución. Esto te permite ejecutar el objeto binario fuera del entorno de desarrollo.

Como recordarás, la regla de compilación java_binary produce un .jar y una secuencia de comandos de shell del wrapper. Para ver el contenido de runner.jar, usa este comando:

jar tf bazel-bin/src/main/java/com/example/cmdline/runner.jar

El contenido es el siguiente:

META-INF/
META-INF/MANIFEST.MF
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class

Como puedes ver, runner.jar contiene Runner.class, pero no su dependencia, Greeting.class. La secuencia de comandos runner que genera Bazel agrega greeter.jar a la ruta de clase, por lo que, si la dejas así, se ejecutará de forma local, pero no se ejecutará de forma independiente en otra máquina. Afortunadamente, la regla java_binary te permite compilar un objeto binario independiente y que se pueda implementar. Para compilarlo, agrega _deploy.jar al nombre del destino:

bazel build //src/main/java/com/example/cmdline:runner_deploy.jar

Bazel produce un resultado similar al siguiente:

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner_deploy.jar up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar
INFO: Elapsed time: 1.700s, Critical Path: 0.23s

Acabas de compilar runner_deploy.jar, que puedes ejecutar de forma independiente desde tu entorno de desarrollo, ya que contiene las dependencias del entorno de ejecución necesarias. Para ver el contenido de este JAR independiente, usa el mismo comando que antes:

jar tf bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar

El contenido incluye todas las clases necesarias para ejecutar:

META-INF/
META-INF/MANIFEST.MF
build-data.properties
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class
com/example/Greeting.class

Lecturas adicionales

Para obtener más información, consulta los siguientes recursos:

¡Feliz compilación!