Construir C ++ com bazel

Introdução e Motivação


Recentemente, surgiram posts no Habr que cmake e c ++ são amigos, são dados exemplos de como coletar bibliotecas apenas de cabeçalho e não apenas, mas não há uma visão geral de pelo menos alguns novos sistemas de construção - bazel, buck, gn e outros. Se você, como eu, escreve em C ++ em 2k20, sugiro que você se familiarize com o bazel como um sistema de construção para um projeto em c ++.

Vamos deixar a questão de que cmake e outros sistemas existentes são ruins e nos concentrarmos no que o próprio bazel pode fazer. Para decidir o que é melhor especificamente para você, deixo especificamente para você.

Vamos começar com definição e motivação. O Bazel é um sistema multilíngue de criação do Google que pode criar projetos em c ++. Por que deveríamos olhar para outro sistema de compilação? Primeiramente, como alguns projetos grandes já estão indo para ela, por exemplo, Tensorflow, Kubernetes e Gtest, e, consequentemente, para integrar-se a eles, você já deve poder usar o bazel. Em segundo lugar, além do google bazel, ainda usa a spaceX, a nvidia e outras empresas a julgar por suas performances no bazelcon. Finalmente, o bazel é um projeto de código aberto bastante popular no github, então vale a pena dar uma olhada e experimentá-lo.

Exemplo 1. Trivial


Existe o main.cc e você precisa compilá-lo:

main.cc

#include <iostream>

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

Tudo começa com uma declaração da área de trabalho. Em termos de espaço de trabalho do bazel, este é o diretório em que todos os seus arquivos de origem estão localizados. Para designar esse espaço de trabalho, você precisa criar um arquivo vazio com o nome WORKSPACE no diretório que precisamos, geralmente esse é o diretório src.

A unidade mínima para organizar o código no bazel é um pacote. O pacote é definido pelo diretório de origem e um arquivo BUILD especial que descreve como essas fontes são montadas.

Adicione o pacote principal ao nosso projeto:



No arquivo BUILD, agora devemos descrever o que queremos construir do nosso principal. Naturalmente, queremos compilar um binário executável, portanto, usaremos a regra cc_binary. O Bazel já suporta C ++ pronto para uso, portanto, já existe um certo conjunto de regras para a criação de objetivos em c ++; conheceremos o resto mais tarde.

Adicione a regra cc_binary ao arquivo BUILD, ele tem um nome que terá um arquivo executável e uma matriz de fontes que serão passadas para o compilador. Tudo isso é descrito em starlark, que é um python truncado.

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

Bazel, diferentemente do cmake, não é baseado em comandos, mas permite descrever declarativamente dependências por meio de regras. Essencialmente, as regras associam vários artefatos a uma operação específica. Utilizando-os, o bazel constrói um gráfico de comando que, em seguida, armazena em cache e executa. No nosso caso, o arquivo de origem main.cc está associado a uma operação de compilação, cujo resultado será o artefato hello_world - um arquivo executável binário.

Para obter nosso executável agora, precisamos ir para o diretório com espaço de trabalho e digitar:

bazel build //main:hello_world

O sistema de construção aceita o comando de construção e o caminho para nosso objetivo, começando na raiz do nosso projeto.

O binário resultante estará localizado em bazel-bin / main / hello_world.

Exemplo 2. Construa com sua biblioteca


Felizmente, ninguém precisa de projetos tão simples, então vamos ver como adicionar funcionalidade ao nosso projeto. Adicione uma biblioteca que será construída separadamente e vinculada ao nosso principal.

Que seja Square, uma biblioteca que fornecerá uma função quadrática inequívoca. Adicionar uma nova biblioteca significa adicionar um novo pacote, vamos chamá-lo de quadrado.



square.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;
}

Preste atenção à conexão do arquivo de cabeçalho, eu o faço pelo caminho do espaço de trabalho, mesmo que o arquivo esteja no mesmo diretório. Essa abordagem é adotada no guia de estilo do código de cromo, herdado do guia de estilo do Google c ++. Este método permite que você entenda imediatamente de onde o arquivo de cabeçalho está conectado. Não se preocupe, haverá um arquivo, o bazel adicionará caminhos para procurar arquivos de cabeçalho, mas se você não seguir esta regra, os arquivos de cabeçalho poderão não ser encontrados durante a compilação do bazel.

No arquivo BUILD da nossa biblioteca, descrevemos a regra para a construção das bibliotecas cc_library:

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

Aqui, listamos separadamente os arquivos de origem e de cabeçalho e também especificamos a visibilidade em público. O último é necessário para que possamos depender da nossa biblioteca em qualquer lugar do nosso projeto.

No main.cc, usamos nossa biblioteca:

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

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

Mais uma vez, chamo a atenção para o fato de incluirmos o arquivo de cabeçalho da biblioteca através do caminho da área de trabalho. Isso já é absolutamente necessário aqui, porque o bazel usa contêineres Linux sob o capô para garantir um nível mínimo de estanqueidade da montagem e, consequentemente, montará os arquivos de cabeçalho da biblioteca quadrada apenas para que eles fiquem localizados no caminho a partir da área de trabalho.

E descrevemos a dependência na regra de montagem para main na biblioteca quadrada.

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

Todo o nosso programa é montado da mesma maneira que, sem o uso da biblioteca, o próprio bazel entenderá do que depende, criará um gráfico, armazenará em cache os resultados e reconstruirá apenas o que precisa ser reconstruído.

bazel build //main:hello_world

Exemplo 3. Conectando Testes


Como viver sem testes? De jeito nenhum! Para conectar-se ao bazel GTest, que já oferece suporte à montagem com o bazel, é necessário adicionar uma dependência externa. Isso é feito no arquivo 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"
)

Assim como os descolados, eles conectaram a regra git_repository e disseram ao bazel qual versão baixar.

Em seguida, criamos um pacote separado para testes de teste e adicionamos testes à nossa 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);
}

Agora é a vez de definir uma regra para testes.

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

Adicionamos dependências em nossa biblioteca e em gtest_main para que a própria biblioteca gtest nos fornecesse uma implementação do iniciador.

Os testes são executados com o comando:

bazel test //test:unittests

O Bazel fará o download e compilará o GTest, vinculará tudo o que é necessário para os testes e executará os próprios testes.

Menciono que o bazel também sabe como fazer cobertura de código:

bazel coverage //test:unittests

E se você precisar depurar testes, poderá compilar tudo no modo de depuração com caracteres como este:

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

Exemplo 4. Conectando outras bibliotecas que não sabem como fazer bazel


Obviamente, o mundo não é construído apenas com o bazel, então você também precisa conectar outras bibliotecas. Recentemente, no meu projeto, eu precisava de uma biblioteca para analisar os argumentos da linha de comando. Bem, para não me escrever em 2k20 sua própria biblioteca e se distrair do trabalho principal. Eu realmente não quero usar meias medidas, como getops, bem como arrastar impulso para o meu projeto.

Não para publicidade, conectaremos a biblioteca CLI11, que usa nada mais que o padrão stl C ++ 11 e fornece uma interface mais ou menos conveniente.

Uma biblioteca é apenas de cabeçalho, o que facilita a conexão.

Conecte a dependência externa no 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...",
)

Nós adicionamos o diretório de terceiros e o pacote CLI11 para facilitar a criação de dependências nesta biblioteca:

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

Por padrão, o Bazel procurará o arquivo da biblioteca pelo caminho / external / CLI11, para que possamos mudar um pouco os caminhos para conectá-lo via 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;
}

Dependendo do main, adicione "// third_party / CLI11: CLI11" e tudo começará a funcionar.
Não conheço você, mas conectar algumas bibliotecas desconhecidas e usá-las em um projeto c ++ neste formulário me encanta.

Sim, com a biblioteca apenas de cabeçalho, você dirá que tudo é simples, mas com uma biblioteca sem cabeçalho que ainda não foi criada com o bazel, tudo é tão simples. Você simplesmente faz o download via http_archive ou git_repository e adiciona um arquivo BUILD externo no diretório de terceiros, onde descreve como construir sua biblioteca. O Bazel suporta a chamada de qualquer cmd e até a chamada de cmake, através da regra cmake_external.

Exemplo 5. Scripts e Automação


Quem precisa de um projeto em c ++ simples em 2k20 sem scripts para automação? Normalmente, esses scripts são necessários para executar testes de desempenho ou implantar seus artefatos em algum lugar no CI. Bem, geralmente eles são escritos em python.

Para isso, o bazel também é adequado, pois pode ser usado em quase todas as linguagens populares e é projetado para coletar esse solyanka de diferentes linguagens de programação que são frequentemente encontradas em projetos reais.

Vamos conectar o script python que executará o nosso principal.

Adicione o pacote perf-tests:

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

Como uma dependência de dados, adicione a dependência binária 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 executar nossos testes, simplesmente escrevemos:
bazel run //perf-tests:perf_tests

Conclusão e o que não é tocado


Analisamos brevemente o bazel e seus principais recursos para montar arquivos e bibliotecas executáveis, de terceiros e nossos. Para o meu gosto, acontece de forma muito concisa e muito rápida. Não há necessidade de sofrer e procure algum tutorial cmake para fazer alguma coisa trivial e limpar o CmakeCache.

Se você estiver interessado, ainda há muito a perder: buffers de protocolo, desinfetantes, configurações de cadeia de ferramentas para compilar para diferentes plataformas / arquiteturas.

Obrigado pela leitura e espero ter sido útil para você.

All Articles