Bangun C ++ dengan bazel

Pengantar dan Motivasi


Baru-baru ini, posting telah muncul di Habr bahwa cmake dan c ++ adalah teman, contoh diberikan tentang cara mengumpulkan perpustakaan hanya header dan tidak hanya, tetapi tidak ada ikhtisar setidaknya beberapa sistem membangun baru - bazel, buck, gn, dan lainnya. Jika Anda, seperti saya, menulis dalam C ++ di 2k20, maka saya sarankan Anda berkenalan dengan bazel sebagai sistem build untuk proyek c ++.

Kami akan meninggalkan pertanyaan tentang apa cmake dan sistem lain yang ada buruk untuk dan berkonsentrasi pada apa yang bisa dilakukan bazel sendiri. Untuk memutuskan apa yang terbaik khusus untuk Anda, saya meninggalkannya khusus untuk Anda.

Mari kita mulai dengan definisi dan motivasi. Bazel adalah sistem multi-bahasa Google build yang dapat membangun proyek-proyek c ++. Mengapa kita harus melihat sistem build lain? Pertama, karena beberapa proyek besar sudah berjalan kepadanya, misalnya Tensorflow, Kubernetes dan Gtest, dan karenanya, untuk berintegrasi dengan mereka, Anda sudah harus dapat menggunakan bazel. Kedua, selain itu bazel google masih menggunakan spaceX, nvidia dan perusahaan lain yang dinilai dari kinerja mereka di bazelcon. Akhirnya, bazel adalah proyek open source yang cukup populer di github, jadi patut dicoba dan dicoba.

Contoh 1. Sepele


Ada main.cc dan Anda harus mengompilasinya:

main.cc

#include <iostream>

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

Semuanya dimulai dengan deklarasi ruang kerja. Dalam hal ruang kerja bazel, ini adalah direktori tempat semua file sumber Anda berada. Untuk menunjuk ruang kerja ini, Anda perlu membuat file kosong dengan nama WORKSPACE di direktori yang kami butuhkan, biasanya ini adalah direktori src.

Unit minimum untuk mengatur kode di bazel adalah sebuah paket. Paket ini ditentukan oleh direktori sumber dan file BUILD khusus yang menjelaskan bagaimana sumber-sumber ini dikumpulkan.

Tambahkan paket utama ke proyek kami:



Dalam file BUILD, kita sekarang harus menjelaskan apa yang ingin kita bangun dari main kita. Secara alami, kami ingin mengkompilasi biner yang dapat dieksekusi, jadi kami akan menggunakan aturan cc_binary. Bazel sudah mendukung C ++ di luar kotak, jadi sudah ada seperangkat aturan tertentu untuk membangun tujuan c ++, kita akan mengenal sisanya nanti.

Tambahkan aturan cc_binary ke file BUILD, ia memiliki nama yang akan memiliki file yang dapat dieksekusi dan berbagai sumber yang akan diteruskan ke kompiler. Semua ini dijelaskan dalam starlark, yang merupakan python terpotong.

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

Bazel, tidak seperti cmake, tidak didasarkan pada perintah, tetapi memungkinkan deklaratif menjelaskan dependensi melalui aturan. Pada dasarnya, aturan menghubungkan beberapa artefak dengan operasi tertentu. Menggunakannya, bazel membuat grafik perintah, yang kemudian di-cache dan dieksekusi. Dalam kasus kami, file sumber main.cc dikaitkan dengan operasi kompilasi, yang hasilnya adalah artefact hello_world - file biner yang dapat dieksekusi.

Untuk sekarang dapat dieksekusi, kita harus pergi ke direktori dengan ruang kerja dan ketik:

bazel build //main:hello_world

Sistem build menerima perintah build dan jalur ke tujuan kami, mulai dari akar proyek kami.

Biner yang dihasilkan akan berlokasi di bazel-bin / main / hello_world.

Contoh 2. Bangun dengan perpustakaan Anda


Untungnya, tidak ada yang membutuhkan proyek sederhana seperti itu, jadi mari kita lihat bagaimana cara menambahkan fungsionalitas ke proyek kami. Tambahkan perpustakaan yang akan dibangun secara terpisah dan ditautkan ke utama kami.

Biarkan Square, perpustakaan yang akan menyediakan fungsi kuadrat yang jelas. Menambahkan pustaka baru berarti menambahkan paket baru, mari kita sebut saja kuadrat.



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

Perhatikan koneksi file header, saya melakukannya melalui jalur dari ruang kerja, meskipun file tersebut ada di direktori yang sama. Pendekatan ini diadopsi dalam panduan gaya kode kromium, yang diwarisi dari panduan gaya google c ++. Metode ini memungkinkan Anda untuk segera memahami dari mana file header terhubung. Jangan khawatir, akan ada file, bazel akan menambahkan path untuk mencari file header, tetapi jika Anda tidak mengikuti aturan ini, maka file header mungkin tidak ditemukan selama pembuatan bazel.

Dalam file BUILD perpustakaan kami, kami menjelaskan aturan untuk membangun perpustakaan cc_library:

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

Di sini kami mencantumkan secara terpisah sumber dan file header, dan juga menentukan visibilitas di depan umum. Yang terakhir ini diperlukan agar kami dapat bergantung pada perpustakaan kami di mana saja dalam proyek kami.

Di main.cc kami menggunakan perpustakaan kami:

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

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

Sekali lagi, saya menarik perhatian pada fakta bahwa kami menyertakan file header perpustakaan melalui path dari workspace. Ini sudah mutlak diperlukan di sini, karena bazel menggunakan wadah Linux di bawah tenda untuk memastikan tingkat keketatan minimum perakitan dan, karenanya, akan memasang file header pustaka kuadrat hanya agar mereka terletak melalui jalur dari ruang kerja.

Dan kami menggambarkan ketergantungan dalam aturan perakitan untuk utama di perpustakaan kuadrat.

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

Seluruh program kami dirakit dengan cara yang sama seperti tanpa menggunakan perpustakaan, bazel sendiri akan memahami apa yang tergantung, membangun grafik, menyimpan hasil dan membangun kembali hanya apa yang perlu dibangun kembali.

bazel build //main:hello_world

Contoh 3. Menghubungkan tes


Bagaimana cara hidup tanpa tes? Tidak mungkin! Untuk terhubung ke bazel GTest, yang omong-omong sudah mendukung perakitan dengan bazel, Anda perlu menambahkan ketergantungan eksternal. Ini dilakukan dalam file 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"
)

Seperti halnya hipsters, mereka menghubungkan aturan git_repository dan memberi tahu bazel versi mana yang harus diunduh.

Selanjutnya, kami membuat paket terpisah untuk tes pengujian dan menambahkan tes ke perpustakaan kami ke dalamnya:

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

Sekarang giliran untuk menentukan aturan untuk pengujian.

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

Kami menambahkan dependensi pada perpustakaan kami dan pada gtest_main sehingga perpustakaan gtest itu sendiri akan memberi kami implementasi peluncur.

Tes dijalankan dengan perintah:

bazel test //test:unittests

Bazel akan mengunduh dan membangun GTest sendiri, menautkan semua yang diperlukan untuk pengujian dan menjalankan tes itu sendiri.

Saya menyebutkan bahwa bazel juga tahu bagaimana melakukan cakupan kode:

bazel coverage //test:unittests

Dan jika Anda perlu melakukan debug tes, maka Anda dapat mengkompilasi semuanya dalam mode debug dengan karakter seperti ini:

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

Contoh 4. Menghubungkan perpustakaan lain yang tidak tahu cara bazel


Tentu saja, dunia tidak dibangun di atas bazel saja, jadi Anda harus dapat menghubungkan perpustakaan lain juga. Baru-baru ini, dalam proyek saya, saya membutuhkan perpustakaan untuk mengurai argumen baris perintah. Nah, jangan menulis kepada saya di 2k20 perpustakaan Anda sendiri dan terganggu dari pekerjaan utama. Saya benar-benar tidak ingin menggunakan setengah tindakan, seperti Getops, serta menyeret dorongan ke dalam proyek saya.

Bukan untuk iklan, kami akan menghubungkan perpustakaan CLI11, yang menggunakan tidak lebih dari stl standar C ++ 11 dan menyediakan antarmuka yang lebih atau kurang nyaman.

Pustaka adalah hanya header, yang membuat menghubungkannya sangat mudah.

Hubungkan ketergantungan eksternal di 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...",
)

Kami menambahkan direktori pihak ketiga dan menambahkan paket CLI11 untuk kenyamanan membangun dependensi pada perpustakaan ini:

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

Bazel secara default akan mencari file library dengan path / eksternal / CLI11 sehingga kami mengubah path sedikit untuk menghubungkannya melalui 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;
}

Bergantung pada main, tambahkan "// third_party / CLI11: CLI11" dan semuanya mulai bekerja.
Saya tidak tahu tentang Anda, tetapi menghubungkan beberapa perpustakaan yang tidak dikenal dan menggunakannya dalam proyek c ++ dalam formulir ini membuat saya senang.

Ya, dengan pustaka header-saja Anda akan mengatakan bahwa semuanya sederhana, tetapi dengan pustaka non-header-saja yang belum dibangun dengan bazel semuanya sama sederhana. Anda cukup mengunduhnya melalui http_archive atau git_repository dan menambahkan file BUILD eksternal ke direktori pihak ketiga, tempat Anda menjelaskan cara membangun perpustakaan Anda. Bazel mendukung memanggil cmd dan bahkan memanggil cmake, melalui aturan cmake_external.

Contoh 5. Skrip dan otomatisasi


Siapa yang butuh proyek di bare c ++ di 2k20 tanpa skrip untuk otomatisasi? Biasanya, skrip seperti itu diperlukan untuk menjalankan tes perf atau untuk menyebarkan artefak Anda di suatu tempat ke CI. Yah, biasanya mereka ditulis dengan python.

Untuk ini, bazel juga cocok, karena dapat digunakan di hampir semua bahasa populer dan dirancang untuk mengumpulkan solyanka seperti itu dari berbagai bahasa pemrograman yang begitu sering ditemukan dalam proyek nyata.

Mari kita sambungkan skrip python yang akan menjalankan main kita.

Tambahkan paket tes perf:

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

Sebagai ketergantungan data, tambahkan dependensi biner 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))

Untuk menjalankan tes kami, kami cukup menulis:
bazel run //perf-tests:perf_tests

Kesimpulan dan apa yang tidak disentuh


Kami secara singkat melihat bazel dan fitur utamanya untuk merakit file dan perpustakaan yang dapat dieksekusi, baik pihak ketiga maupun milik kami. Untuk seleraku, ternyata cukup singkat dan sangat cepat. Tidak perlu menderita dan mencari tutorial cmake untuk melakukan beberapa hal sepele dan membersihkan CmakeCache.

Jika Anda tertarik, masih banyak yang tersisa: buffer protokol, pembersih, pengaturan toolchain untuk dikompilasi untuk berbagai platform / arsitektur.

Terima kasih telah membaca, dan saya harap saya bermanfaat bagi Anda.

All Articles