Construire C ++ avec bazel

Introduction et motivation


Récemment, des articles sont apparus sur Habr que cmake et c ++ sont amis, des exemples sont donnés sur la façon de collecter des bibliothèques uniquement en-tête et pas seulement, mais il n'y a pas de vue d'ensemble d'au moins certains nouveaux systèmes de construction - bazel, buck, gn et autres. Si vous, comme moi, écrivez en C ++ en 2k20, alors je vous suggère de vous familiariser avec bazel comme système de construction pour un projet c ++.

Nous laisserons la question de savoir à quoi cmake et les autres systèmes existants sont mauvais et nous nous concentrerons sur ce que le bazel lui-même peut faire. Pour décider ce qui vous convient le mieux, je vous le laisse spécialement.

Commençons par la définition et la motivation. Bazel est un système de build Google multilingue qui peut créer des projets c ++. Pourquoi devrions-nous même envisager un autre système de construction? Tout d'abord, parce que certains grands projets lui sont déjà destinés, par exemple Tensorflow, Kubernetes et Gtest, et en conséquence, pour s'intégrer avec eux, vous devez déjà pouvoir utiliser bazel. Deuxièmement, outre google bazel utilise toujours spaceX, nvidia et d'autres sociétés à en juger par leurs performances sur bazelcon. Enfin, bazel est un projet open source assez populaire sur github, donc ça vaut vraiment le coup d'oeil et essayez-le.

Exemple 1. Trivial


Il y a main.cc et vous devez le compiler:

main.cc

#include <iostream>

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

Tout commence par une déclaration d'espace de travail. En termes d'espace de travail bazel, c'est le répertoire dans lequel se trouvent tous vos fichiers source. Pour désigner cet espace de travail, vous devez créer un fichier vide avec le nom WORKSPACE dans le répertoire dont nous avons besoin, généralement c'est le répertoire src.

L'unité minimale pour organiser le code dans Bazel est un package. Le package est défini par le répertoire source et un fichier BUILD spécial qui décrit comment ces sources sont assemblées.

Ajoutez le package principal à notre projet:



Dans le fichier BUILD, nous devons maintenant décrire ce que nous voulons construire à partir de notre principal. Naturellement, nous voulons compiler un binaire exécutable, nous allons donc utiliser la règle cc_binary. Bazel prend déjà en charge C ++, il existe donc déjà un certain ensemble de règles pour la construction d'objectifs c ++, nous apprendrons le reste plus tard.

Ajoutez la règle cc_binary au fichier BUILD, elle a un nom qui aura un fichier exécutable et un tableau de sources qui seront passées au compilateur. Tout cela est décrit dans starlark, qui est un python tronqué.

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

Bazel, contrairement à cmake, n'est pas basé sur des commandes, mais permet de décrire de manière déclarative des dépendances à travers des règles. Essentiellement, les règles associent plusieurs artefacts à une opération spécifique. En les utilisant, bazel construit un graphe de commandes, qui met ensuite en cache et s'exécute. Dans notre cas, le fichier source main.cc est associé à une opération de compilation, dont le résultat est l'artefact hello_world - un fichier exécutable binaire.

Pour obtenir maintenant notre exécutable, nous devons aller dans le répertoire avec workspace et taper:

bazel build //main:hello_world

Le système de build accepte la commande build et le chemin vers notre objectif, en partant de la racine de notre projet.

Le binaire résultant sera situé dans bazel-bin / main / hello_world.

Exemple 2. Construisez avec votre bibliothèque


Heureusement, personne n'a besoin de projets aussi simples, alors voyons comment ajouter des fonctionnalités à notre projet. Ajoutez une bibliothèque qui sera construite séparément et liée à notre principale.

Que ce soit Square, une bibliothèque qui fournira une fonction d'équerrage sans ambiguïté. Ajouter une nouvelle bibliothèque signifie ajouter un nouveau paquet, appelons-le également carré.



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

Faites attention à la connexion du fichier d'en-tête, je le fais via le chemin depuis l'espace de travail, même si le fichier se trouve dans le même répertoire. Cette approche est adoptée dans le guide de style du code du chrome, hérité du guide de style google c ++. Cette méthode vous permet de comprendre immédiatement d'où le fichier d'en-tête est connecté. Ne vous inquiétez pas, il y aura un fichier, bazel ajoutera des chemins pour rechercher les fichiers d'en-tête, mais si vous ne suivez pas cette règle, les fichiers d'en-tête peuvent ne pas être trouvés pendant la construction de bazel.

Dans le fichier BUILD de notre bibliothèque, nous décrivons la règle de construction des bibliothèques cc_library:

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

Ici, nous listons séparément les fichiers source et d'en-tête, et spécifions également la visibilité en public. Ce dernier est nécessaire pour que nous puissions dépendre de notre bibliothèque n'importe où dans notre projet.

Dans main.cc, nous utilisons notre bibliothèque:

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

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

Encore une fois, j'attire l'attention sur le fait que nous incluons le fichier d'en-tête de la bibliothèque via le chemin depuis l'espace de travail. C'est déjà absolument nécessaire ici, car bazel utilise des conteneurs Linux sous le capot pour assurer un niveau minimum d'étanchéité de l'assemblage et, en conséquence, il montera les fichiers d'en-tête de bibliothèque carrés juste pour qu'ils soient situés à travers le chemin depuis l'espace de travail.

Et nous décrivons la dépendance dans la règle d'assemblage de main sur la bibliothèque carrée.

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

L'ensemble de notre programme est assemblé de la même manière que sans utiliser la bibliothèque, bazel lui-même comprendra de quoi il dépend, construira un graphe, mettra en cache les résultats et reconstruira uniquement ce qui doit être reconstruit.

bazel build //main:hello_world

Exemple 3. Connexion de tests


Comment vivre sans tests? En aucune façon! Pour vous connecter à bazel GTest, qui en passant prend déjà en charge l'assemblage avec bazel, vous devez ajouter une dépendance externe. Cela se fait dans le fichier 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"
)

Tout comme les hipsters, ils ont connecté la règle git_repository et ont dit à bazel quelle version télécharger.

Ensuite, nous créons un package séparé pour les tests et ajoutons des tests à notre bibliothèque:

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);
}

C'est maintenant au tour de définir une règle pour les tests.

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

Nous avons ajouté des dépendances à notre bibliothèque et à gtest_main afin que la bibliothèque gtest elle-même nous fournisse une implémentation du lanceur.

Les tests sont exécutés avec la commande:

bazel test //test:unittests

Bazel téléchargera et construira GTest lui-même, reliera tout ce qui est nécessaire pour les tests et exécutera les tests eux-mêmes.

Je mentionne que bazel sait aussi faire une couverture de code:

bazel coverage //test:unittests

Et si vous devez déboguer des tests, vous pouvez tout compiler en mode débogage avec des caractères comme celui-ci:

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

Exemple 4. Connexion d'autres bibliothèques qui ne savent pas comment bazel


Bien sûr, le monde n'est pas construit uniquement sur bazel, vous devez donc pouvoir également connecter d'autres bibliothèques. Récemment, dans mon projet, j'avais besoin d'une bibliothèque pour analyser les arguments de la ligne de commande. Eh bien, ne m'écrivez pas en 2k20 votre propre bibliothèque et soyez distrait du travail principal. Je ne veux vraiment pas utiliser de demi-mesures, comme les getops, ainsi que faire glisser le boost dans mon projet.

Pas pour la publicité, nous allons connecter la bibliothèque CLI11, qui n'utilise rien de plus que le standard stl C ++ 11 et fournit une interface plus ou moins pratique.

Une bibliothèque est uniquement en-tête, ce qui rend sa connexion particulièrement facile.

Connectez la dépendance externe dans 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...",
)

Nous ajoutons le répertoire tiers et ajoutons le package CLI11 pour faciliter la création de dépendances sur cette bibliothèque:

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

Bazel recherchera par défaut le fichier de bibliothèque par le chemin / external / CLI11 afin que nous modifions un peu les chemins pour le connecter 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;
}

Selon le principal, ajoutez "// third_party / CLI11: CLI11" et tout commence à fonctionner.
Je ne vous connais pas, mais connecter une bibliothèque inconnue et l'utiliser dans un projet c ++ sous cette forme me ravit.

Oui, avec la bibliothèque d'en-tête uniquement, vous direz que tout est simple, mais avec une bibliothèque non d'en-tête uniquement qui n'est pas encore construite avec bazel, tout est tout aussi simple. Il vous suffit de le télécharger via http_archive ou git_repository et d'y ajouter un fichier BUILD externe dans le répertoire tiers, où vous décrivez comment construire votre bibliothèque. Bazel prend en charge l'appel de n'importe quel cmd et même l'appel de cmake, via la règle cmake_external.

Exemple 5. Scripts et automatisation


Qui a besoin d'un projet en bare c ++ en 2k20 sans scripts pour l'automatisation? En règle générale, ces scripts sont nécessaires pour exécuter des tests de performances ou pour déployer vos artefacts quelque part sur CI. Eh bien, ils sont généralement écrits en python.

Pour cela, bazel convient également, car il peut être utilisé dans presque tous les langages populaires et est conçu pour collecter un tel solyanka à partir de différents langages de programmation que l'on trouve si souvent dans les projets réels.

Connectons le script python qui exécutera notre main.

Ajoutez le package perf-tests:

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

En tant que dépendance de données, ajoutez la dépendance binaire 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))

Pour exécuter nos tests, nous écrivons simplement:
bazel run //perf-tests:perf_tests

Conclusion et ce qui n'est pas touché


Nous avons brièvement examiné le bazel et ses principales fonctionnalités pour assembler des fichiers exécutables et des bibliothèques, tant tierces que les nôtres. À mon goût, cela se révèle assez concis et très rapidement. Pas besoin de souffrir et de chercher un tutoriel cmake pour faire quelque chose de trivial et nettoyer CmakeCache.

Si vous êtes intéressé, il reste encore beaucoup à faire: connexion de tampons de protocole, désinfectants, mise en place d'une chaîne d'outils à compiler pour différentes plates-formes / architectures.

Merci d'avoir lu et j'espère vous avoir été utile.

All Articles