Erstellen Sie C ++ mit Bazel

Einführung und Motivation


Kürzlich sind auf Habr Beiträge erschienen, in denen cmake und c ++ Freunde sind. Es werden Beispiele dafür gegeben, wie man nur Header-Bibliotheken sammelt und nicht nur, aber es gibt keinen Überblick über zumindest einige neue Build-Systeme - bazel, buck, gn und andere. Wenn Sie wie ich in 2k20 in C ++ schreiben, sollten Sie sich mit bazel als Build-System für ein C ++ - Projekt vertraut machen.

Wir werden die Frage hinterlassen, wofür cmake und andere vorhandene Systeme schlecht sind, und uns darauf konzentrieren, was Bazel selbst tun kann. Um zu entscheiden, was speziell für Sie am besten ist, überlasse ich es speziell für Sie.

Beginnen wir mit Definition und Motivation. Bazel ist ein mehrsprachiges Google-Build-System, mit dem C ++ - Projekte erstellt werden können. Warum sollten wir uns überhaupt ein anderes Build-System ansehen? Erstens, da einige große Projekte bereits an sie gehen, zum Beispiel Tensorflow, Kubernetes und Gtest, und dementsprechend müssen Sie Bazel bereits verwenden können, um sich in sie zu integrieren. Zweitens verwendet Google Bazel neben Google noch SpaceX, NVIDIA und andere Unternehmen, gemessen an ihren Leistungen auf Bazelcon. Schließlich ist bazel ein ziemlich beliebtes Open-Source-Projekt auf Github, also ist es definitiv einen Blick wert und probieren Sie es aus.

Beispiel 1. Trivial


Es gibt main.cc und Sie müssen es kompilieren:

main.cc

#include <iostream>

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

Alles beginnt mit einer Arbeitsbereichserklärung. In Bezug auf den Bazel-Arbeitsbereich ist dies das Verzeichnis, in dem sich alle Ihre Quelldateien befinden. Um diesen Arbeitsbereich zu bestimmen, müssen Sie eine leere Datei mit dem Namen WORKSPACE in dem von uns benötigten Verzeichnis erstellen. In der Regel ist dies das src-Verzeichnis.

Die Mindesteinheit für die Organisation von Code in Bazel ist ein Paket. Das Paket wird durch das Quellverzeichnis und eine spezielle BUILD-Datei definiert, die beschreibt, wie diese Quellen zusammengesetzt werden.

Fügen Sie das Hauptpaket zu unserem Projekt hinzu:



In der BUILD-Datei müssen wir nun beschreiben, was wir aus unserem Hauptpaket erstellen möchten. Natürlich möchten wir eine ausführbare Binärdatei kompilieren, daher verwenden wir die Regel cc_binary. Bazel unterstützt C ++ bereits ab Werk, daher gibt es bereits bestimmte Regeln für die Erstellung von C ++ - Zielen. Den Rest werden wir später kennenlernen.

Fügen Sie der BUILD-Datei die Regel cc_binary hinzu. Sie enthält einen Namen mit einer ausführbaren Datei und ein Array von Quellen, die an den Compiler übergeben werden. All dies wird in Starlark beschrieben, einer abgeschnittenen Python.

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

Bazel basiert im Gegensatz zu cmake nicht auf Befehlen, sondern ermöglicht die deklarative Beschreibung von Abhängigkeiten durch Regeln. Im Wesentlichen ordnen Regeln einer bestimmten Operation mehrere Artefakte zu. Mit ihnen erstellt bazel ein Befehlsdiagramm, das dann zwischengespeichert und ausgeführt wird. In unserem Fall ist die Quelldatei main.cc einer Kompilierungsoperation zugeordnet, deren Ergebnis das Artefakt hello_world ist - eine binäre ausführbare Datei.

Um unsere ausführbare Datei zu erhalten, müssen wir in das Verzeichnis mit dem Arbeitsbereich gehen und Folgendes eingeben:

bazel build //main:hello_world

Das Build-System akzeptiert den Build-Befehl und den Pfad zu unserem Ziel, beginnend mit der Wurzel unseres Projekts.

Die resultierende Binärdatei befindet sich unter bazel-bin / main / hello_world.

Beispiel 2. Erstellen Sie mit Ihrer Bibliothek


Glücklicherweise braucht niemand so einfache Projekte. Lassen Sie uns sehen, wie wir unserem Projekt Funktionen hinzufügen können. Fügen Sie eine Bibliothek hinzu, die separat erstellt und mit unserer Hauptbibliothek verknüpft wird.

Sei es Square, eine Bibliothek, die eine eindeutige Quadrierungsfunktion bietet. Das Hinzufügen einer neuen Bibliothek bedeutet das Hinzufügen eines neuen Pakets. Nennen wir es auch Quadrat.



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

Achten Sie auf die Verbindung der Header-Datei, ich mache es über den Pfad vom Arbeitsbereich, obwohl sich die Datei im selben Verzeichnis befindet. Dieser Ansatz wird im Chromcode-Styleguide übernommen, der vom Google C ++ - Styleguide übernommen wurde. Mit dieser Methode können Sie sofort nachvollziehen, von wo aus die Header-Datei verbunden ist. Keine Sorge, es wird eine Datei geben. Bazel fügt Pfade hinzu, um nach Header-Dateien zu suchen. Wenn Sie diese Regel jedoch nicht befolgen, werden Header-Dateien möglicherweise während des Bazel-Builds nicht gefunden.

In der BUILD-Datei unserer Bibliothek beschreiben wir die Regel zum Erstellen der cc_library-Bibliotheken:

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

Hier listen wir die Quell- und Header-Dateien getrennt auf und geben auch die Sichtbarkeit in der Öffentlichkeit an. Letzteres ist notwendig, damit wir uns überall in unserem Projekt auf unsere Bibliothek verlassen können.

In main.cc verwenden wir unsere Bibliothek:

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

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

Ich mache erneut darauf aufmerksam, dass wir die Header-Datei der Bibliothek über den Pfad vom Arbeitsbereich einschließen. Dies ist hier bereits unbedingt erforderlich, da bazel Linux-Container unter der Haube verwendet, um ein Mindestmaß an Dichtheit der Baugruppe sicherzustellen, und dementsprechend die quadratischen Bibliotheksheaderdateien so bereitstellt, dass sie sich über den Pfad vom Arbeitsbereich befinden.

Und wir beschreiben die Abhängigkeit in der Assembly-Regel für main von der quadratischen Bibliothek.

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

Unser gesamtes Programm wird auf die gleiche Weise zusammengestellt wie ohne Verwendung der Bibliothek. Bazel selbst wird verstehen, worauf es ankommt, ein Diagramm erstellen, die Ergebnisse zwischenspeichern und nur das neu erstellen, was neu erstellt werden muss.

bazel build //main:hello_world

Beispiel 3. Verbindungstests


Wie kann man ohne Tests leben? Auf keinen Fall! Um eine Verbindung zu bazel GTest herzustellen, das übrigens bereits die Montage mit bazel unterstützt, müssen Sie eine externe Abhängigkeit hinzufügen. Dies erfolgt in der WORKSPACE-Datei:

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"
)

Genau wie Hipster haben sie die git_repository-Regel verbunden und bazel mitgeteilt, welche Version heruntergeladen werden soll.

Als Nächstes erstellen wir ein separates Paket für

Testtests und fügen unserer Bibliothek Tests hinzu: 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);
}

Jetzt ist es an der Zeit, eine Regel für Tests zu definieren.

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

Wir haben Abhängigkeiten von unserer Bibliothek und von gtest_main hinzugefügt, damit die gtest-Bibliothek selbst uns eine Launcher-Implementierung bietet.

Tests werden mit dem folgenden Befehl ausgeführt:

bazel test //test:unittests

Bazel wird GTest selbst herunterladen und erstellen, alles verknüpfen, was für die Tests benötigt wird, und die Tests selbst ausführen.

Ich erwähne, dass bazel auch weiß, wie man Code abdeckt:

bazel coverage //test:unittests

Und wenn Sie Tests debuggen müssen, können Sie alles im Debug-Modus mit folgenden Zeichen kompilieren:

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

Beispiel 4. Anschließen anderer Bibliotheken, die nicht wissen, wie man bazelt


Natürlich ist die Welt nicht nur auf Bazel aufgebaut, daher müssen Sie auch andere Bibliotheken verbinden können. Kürzlich brauchte ich in meinem Projekt eine Bibliothek, um Befehlszeilenargumente zu analysieren. Nun, um mir in 2k20 nicht deine eigene solche Bibliothek zu schreiben und dich von der Hauptarbeit ablenken zu lassen. Ich möchte wirklich keine halben Sachen wie getops verwenden und Boost in mein Projekt ziehen.

Nicht für Werbung, wir verbinden die CLI11-Bibliothek, die nur den Standard-Standard C ++ 11 verwendet und eine mehr oder weniger bequeme Oberfläche bietet.

Eine Bibliothek ist nur für Header gedacht, was das Verbinden besonders einfach macht.

Verbinden Sie die externe Abhängigkeit in 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...",
)

Wir fügen das Verzeichnis eines Drittanbieters hinzu und fügen das CLI11-Paket hinzu, um Abhängigkeiten von dieser Bibliothek zu erstellen:

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

Bazel sucht standardmäßig nach der Bibliotheksdatei entlang des Pfads / external / CLI11, sodass wir die Pfade ein wenig ändern, um sie über CLI11 / zu verbinden.

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

Fügen Sie je nach Haupt "// Drittanbieter / CLI11: CLI11" hinzu, und alles beginnt zu funktionieren.
Ich weiß nichts über Sie, aber das Verbinden einer unbekannten Bibliothek und deren Verwendung in einem C ++ - Projekt in dieser Form freut mich.

Ja, mit der Nur-Header-Bibliothek werden Sie sagen, dass alles einfach ist, aber mit einer Nicht-Header-Bibliothek, die noch nicht mit Bazel erstellt wurde, ist alles genauso einfach. Sie laden es einfach über http_archive oder git_repository herunter und fügen eine externe BUILD-Datei im Verzeichnis eines Drittanbieters hinzu, in der Sie beschreiben, wie Sie Ihre Bibliothek erstellen. Bazel unterstützt das Aufrufen von cmd und sogar das Aufrufen von cmake über die Regel cmake_external.

Beispiel 5. Skripte und Automatisierung


Wer braucht ein Projekt in Bare C ++ in 2k20 ohne Skripte zur Automatisierung? In der Regel werden solche Skripte benötigt, um Perf-Tests auszuführen oder Ihre Artefakte irgendwo in CI bereitzustellen. Normalerweise sind sie in Python geschrieben.

Hierfür eignet sich auch Bazel, da es in fast allen gängigen Sprachen verwendet werden kann und dazu dient, solche Solyanka aus verschiedenen Programmiersprachen zu sammeln, die so häufig in realen Projekten vorkommen.

Verbinden wir das Python-Skript, mit dem unser Main ausgeführt wird.

Fügen Sie das Perf-Test-Paket hinzu:

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

Fügen Sie als Datenabhängigkeit die binäre Abhängigkeit hello_world hinzu.

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

Um unsere Tests durchzuführen, schreiben wir einfach:
bazel run //perf-tests:perf_tests

Fazit und was nicht berührt wird


Wir haben uns kurz mit bazel und seinen Hauptfunktionen zum Zusammenstellen ausführbarer Dateien und Bibliotheken befasst, sowohl von Drittanbietern als auch von uns. Für meinen Geschmack fällt es ziemlich prägnant und sehr schnell aus. Sie müssen nicht leiden und nach einem cmake-Tutorial suchen, um etwas Triviales zu tun und CmakeCache zu bereinigen.

Wenn Sie interessiert sind, bleibt noch viel übrig: Verbinden von Protokollpuffern, Desinfektionsprogrammen, Einrichten einer Toolkette zum Kompilieren für verschiedene Plattformen / Architekturen.

Danke fürs Lesen und ich hoffe, ich war nützlich für Sie.

All Articles