Construir C ++ con bazel

Introducción y motivación


Recientemente, han aparecido publicaciones en Habr que cmake y c ++ son amigos, se dan ejemplos de cómo recopilar bibliotecas de solo encabezado y no solo, pero no hay una descripción general de al menos algunos sistemas de compilación nuevos: bazel, buck, gn y otros. Si usted, como yo, escribe en C ++ en 2k20, le sugiero que se familiarice con Bazel como un sistema de compilación para un proyecto de C ++.

Dejaremos la pregunta de qué cmake y otros sistemas existentes son malos y nos concentraremos en lo que Bazel puede hacer. Para decidir qué es lo mejor específicamente para ti, lo dejo específicamente para ti.

Comencemos con la definición y la motivación. Bazel es un sistema de compilación multilingüe de Google que puede construir proyectos en c ++. ¿Por qué deberíamos mirar a otro sistema de compilación? En primer lugar, debido a que algunos proyectos grandes ya están yendo hacia ella, por ejemplo Tensorflow, Kubernetes y Gtest, y en consecuencia, para integrarse con ellos, ya debe poder usar bazel. En segundo lugar, además de Google, Bazel todavía usa spaceX, nvidia y otras compañías a juzgar por sus actuaciones en bazelcon. Finalmente, bazel es un proyecto de código abierto bastante popular en github, por lo que definitivamente vale la pena echarle un vistazo y probarlo.

Ejemplo 1. Trivial


Hay main.cc y necesitas compilarlo:

main.cc

#include <iostream>

int main() {
   std::cout << "Hello, habr" << std::endl;
   return 0;
}

Todo comienza con una declaración de espacio de trabajo. En términos de espacio de trabajo de Bazel, este es el directorio en el que se encuentran todos sus archivos de origen. Para designar este espacio de trabajo, debe crear un archivo vacío con el nombre ESPACIO DE TRABAJO en el directorio que necesitamos, generalmente este es el directorio src.

La unidad mínima para organizar el código en bazel es un paquete. El paquete está definido por el directorio fuente y un archivo BUILD especial que describe cómo se ensamblan estas fuentes.

Agregue el paquete principal a nuestro proyecto:



en el archivo BUILD, ahora debemos describir lo que queremos construir desde nuestro main. Naturalmente, queremos compilar un binario ejecutable, por lo que utilizaremos la regla cc_binary. Bazel ya admite C ++ fuera de la caja, por lo que ya hay un cierto conjunto de reglas para construir objetivos de C ++, conoceremos el resto más adelante.

Agregue la regla cc_binary al archivo BUILD, tiene un nombre que tendrá un archivo ejecutable y una matriz de fuentes que se pasarán al compilador. Todo esto se describe en starlark, que es una pitón truncada.

cc_binary(
  name = "hello_world",
  srcs = "main.cc"
)

Bazel, a diferencia de cmake, no se basa en comandos, pero permite describir declarativamente dependencias a través de reglas. Esencialmente, las reglas asocian múltiples artefactos con una operación específica. Utilizándolos, Bazel crea un gráfico de comandos, que luego se almacena en caché y se ejecuta. En nuestro caso, el archivo fuente main.cc está asociado con una operación de compilación, cuyo resultado es el artefacto hello_world, un archivo ejecutable binario.

Para obtener ahora nuestro ejecutable, debemos ir al directorio con espacio de trabajo y escribir:

bazel build //main:hello_world

El sistema de compilación acepta el comando de compilación y el camino hacia nuestro objetivo, comenzando desde la raíz de nuestro proyecto.

El binario resultante se ubicará en bazel-bin / main / hello_world.

Ejemplo 2. Construye con tu biblioteca


Afortunadamente, nadie necesita proyectos tan simples, así que veamos cómo agregar funcionalidad a nuestro proyecto. Agregue una biblioteca que se construirá por separado y se vinculará a nuestra principal.

Deja que sea Square, una biblioteca que proporcionará una función de cuadratura inequívoca. Agregar una nueva biblioteca significa agregar un nuevo paquete, también llamémoslo cuadrado.



cuadrado.h

#ifndef SQUQRE_SQUARE_H_
#define SQUQRE_SQUARE_H_

int Square(int x);

#endif // SQUQRE_SQUARE_H_

square.cc
#include "square/square.h"

int Square(int x) {
  return x * x;
}

Presta atención a la conexión del archivo de encabezado, lo hago a través de la ruta desde el espacio de trabajo, aunque el archivo esté en el mismo directorio. Este enfoque se adopta en la guía de estilo de código de cromo, que se hereda de la guía de estilo google c ++. Este método le permite comprender de inmediato desde dónde está conectado el archivo de encabezado. No se preocupe, habrá un archivo, Bazel agregará rutas para buscar archivos de encabezado, pero si no sigue esta regla, es posible que no se encuentren archivos de encabezado durante la compilación de Bazel.

En el archivo BUILD de nuestra biblioteca, describimos la regla para construir las bibliotecas cc_library:

cc_library(
  name = "square",
  srcs = ["square.cc"],
  hdrs = ["square.h"],
  visibility = ["//visibility:public"]
)

Aquí enumeramos por separado los archivos de origen y encabezado, y también especificamos la visibilidad en público. Esto último es necesario para que podamos depender de nuestra biblioteca en cualquier parte de nuestro proyecto.

En main.cc usamos nuestra biblioteca:

#include <iostream>
#include "square/square.h"

int main() {
  std::cout << "Hello, bazel!" << std::endl;
  std::cout << Square(2) << std::endl;
  return 0;
}

Nuevamente, llamo la atención sobre el hecho de que incluimos el archivo de encabezado de la biblioteca a través de la ruta desde el espacio de trabajo. Esto ya es absolutamente necesario aquí, porque bazel usa contenedores de Linux debajo del capó para garantizar un nivel mínimo de estanqueidad del ensamblaje y, en consecuencia, montará los archivos de encabezado de la biblioteca cuadrada solo para que se ubiquen a través de la ruta desde el espacio de trabajo.

Y describimos la dependencia en la regla de ensamblaje para main en la biblioteca cuadrada.

cc_binary(
  name = "hello_world",
  srcs = ["main.cc"],
  deps = ["//square:square"]
)

Todo nuestro programa se ensambla de la misma manera que sin usar la biblioteca, Bazel comprenderá de qué depende, construirá un gráfico, almacenará en caché los resultados y reconstruirá solo lo que necesita ser reconstruido.

bazel build //main:hello_world

Ejemplo 3. Pruebas de conexión


¿Cómo vivir sin pruebas? ¡De ninguna manera! Para conectarse a bazel GTest, que por cierto ya admite el ensamblaje con bazel, debe agregar una dependencia externa. Esto se hace en el archivo WORKSPACE:

load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")

git_repository(
  name = "googletest",
  remote = "https://github.com/google/googletest",
  tag = "release-1.8.1"
)

Al igual que los hipsters, conectaron la regla git_repository y le dijeron a bazel qué versión descargar.

A continuación, creamos un paquete separado para pruebas de prueba y agregamos pruebas a nuestra biblioteca:

square_unittest.cc

#include "gtest/gtest.h"
#include "square/square.h"

TEST(SquareTests, Basics) {
    EXPECT_EQ(Square(-1), 1);
    EXPECT_EQ(Square(1), 1);
    EXPECT_EQ(Square(2), 4);
}

Ahora es el turno de definir una regla para las pruebas.

cc_test(
  name = "unittests",
  srcs = ["square_unittest.cc"],
  deps = [
   "//square:square",
   "@googletest//:gtest_main"
  ]
)

Agregamos dependencias en nuestra biblioteca y en gtest_main para que la biblioteca gtest nos proporcione una implementación de iniciador.

Las pruebas se ejecutan con el comando:

bazel test //test:unittests

Bazel descargará y creará GTest, vinculará todo lo que se necesita para las pruebas y las ejecutará ellos mismos.

Menciono que Bazel también sabe cómo hacer cobertura de código:

bazel coverage //test:unittests

Y si necesita depurar pruebas, puede compilar todo en modo de depuración con caracteres como este:

bazel build //test:unittests --compilation_mode=dbg -s

Ejemplo 4. Conexión de otras bibliotecas que no saben bazel


Por supuesto, el mundo no se basa solo en Bazel, por lo que también debe poder conectar otras bibliotecas. Recientemente, en mi proyecto, necesitaba una biblioteca para analizar los argumentos de la línea de comandos. Bueno, no me escribas en 2k20 tu propia biblioteca y te distraigas del trabajo principal. Realmente no quiero usar medias tintas, como getops, ni arrastrar impulso a mi proyecto.

No para publicidad, conectaremos la biblioteca CLI11, que usa nada más que el estándar STL C ++ 11 y proporciona una interfaz más o menos conveniente.

Una biblioteca es solo de encabezado, lo que hace que la conexión sea especialmente fácil.

Conecte la dependencia externa en WORKSPACE:

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")

http_file(
  name = "CLI11",
  downloaded_file_path = "CLI11.hpp",
  urls = ["https://github.com/CLIUtils/CLI11/releases/download/v1.9.0/CLI11.hpp"],
  sha256 = "6f0a1d8846ed7fa4c2b66da3eb252aa03d27170258df...",
)

Agregamos el directorio de terceros y agregamos el paquete CLI11 para la conveniencia de crear dependencias en esta biblioteca:

cc_library(
  name = "CLI11",
  hdrs = ["@CLI11//file"],
  strip_include_prefix = "/external/CLI11/file",
  include_prefix = "CLI11",
  linkstatic = True,
  visibility = ["//visibility:public"],
)

Bazel buscará por defecto el archivo de la biblioteca por la ruta / external / CLI11 para que cambiemos un poco las rutas para conectarlo a través de CLI11 /.

main.cc

#include <iostream>

#include "CLI11/CLI11.hpp"
#include "square/square.h"

int main() {
  std::cout << "Hello, bazel!" << std::endl;
  std::cout << Square(2) << std::endl;
  return 0;
}

Dependiendo de main, agregue "// third_party / CLI11: CLI11" y todo comenzará a funcionar.
No sé sobre usted, pero conectar esta biblioteca desconocida y usarla en un proyecto de C ++ de esta forma me deleita.

Sí, con la biblioteca de solo encabezado dirá que todo es simple, pero con una biblioteca que no es solo de encabezado que aún no está construida con bazel, todo es igual de simple. Simplemente descárguelo a través de http_archive o git_repository y agregue un archivo externo BUILD en el directorio de terceros, donde describe cómo construir su biblioteca. Bazel admite llamar a cualquier cmd e incluso llamar a cmake, a través de la regla cmake_external.

Ejemplo 5. Scripts y automatización.


¿Quién necesita un proyecto en c ++ simple en 2k20 sin scripts para la automatización? Por lo general, estos scripts son necesarios para ejecutar pruebas de rendimiento o para implementar sus artefactos en algún lugar de CI. Bueno, generalmente están escritos en python.

Para esto, Bazel también es adecuado, ya que se puede usar en casi todos los lenguajes populares y está diseñado para recopilar tales solyanka de diferentes lenguajes de programación que a menudo se encuentran en proyectos reales.

Vamos a conectar el script de Python que ejecutará nuestro main.

Agregue el paquete de perf-tests:

py_binary(
  name = "perf_tests",
  srcs = ["perf_tests.py"],
  data = [
    "//main:hello_world",
  ],
)

Como dependencia de datos, agregue la dependencia binaria hello_world.

perf_tests.py

import subprocess
import time

start_time = time.time()
process = subprocess.run(['main/hello_world, 'param1', ],
                         stdout=subprocess.PIPE,
                         universal_newlines=True)
end_time = time.time()
print("--- %s seconds ---" % (end_time - start_time))

Para ejecutar nuestras pruebas, simplemente escribimos:
bazel run //perf-tests:perf_tests

Conclusión y lo que no se toca.


Analizamos brevemente a bazel y sus características principales para ensamblar archivos ejecutables y bibliotecas, tanto de terceros como nuestras. Para mi gusto, resulta bastante concisa y muy rápida. No hay necesidad de sufrir y buscar algún tutorial de cmake para hacer algo trivial y limpiar CmakeCache.

Si está interesado, todavía queda mucho por la borda: búferes de protocolo, desinfectantes, configuraciones de cadena de herramientas para compilar para diferentes plataformas / arquitecturas.

Gracias por leer, y espero haberte sido útil.

All Articles