بناء C ++ مع بازل

مقدمة وتحفيز


في الآونة الأخيرة ، ظهرت مشاركات على Habr بأن cmake و c ++ صديقان ، والأمثلة مذكورة على كيفية جمع مكتبات رأس فقط وليس فقط ، ولكن لا توجد نظرة عامة على الأقل لبعض أنظمة البناء الجديدة - bazel و buck و gn وغيرها. إذا كنت مثلي أكتب C ++ في 2k20 ، فأقترح عليك التعرف على بازل كنظام بناء لمشروع c ++.

سنترك السؤال عن ما هو cmake والأنظمة الموجودة الأخرى السيئة والتركيز على ما يمكن أن تفعله بازل نفسها. لتحديد أفضل ما يناسبك ، أتركه لك تحديدًا.

لنبدأ بالتعريف والتحفيز. Bazel هو نظام بناء متعدد اللغات من Google يمكنه بناء مشاريع c ++. لماذا يجب أن ننظر حتى إلى نظام بناء آخر؟ أولاً ، لأن بعض المشاريع الكبيرة تذهب إليها بالفعل ، على سبيل المثال Tensorflow و Kubernetes و Gtest ، وبالتالي ، للاندماج معها ، تحتاج بالفعل إلى استخدام bazel. ثانيًا ، إلى جانب google bazel ، لا تزال تستخدم spaceX و nvidia وشركات أخرى بناءً على عروضها على bazelcon. أخيرًا ، البازيل هو مشروع مفتوح المصدر شائع جدًا على github ، لذلك من المؤكد أنه يستحق نظرة وتجربته.

مثال 1. تافهة


يوجد main.cc وتحتاج إلى

تجميعه : main.cc

#include <iostream>

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

يبدأ كل شيء بإعلان مساحة العمل. من حيث مساحة عمل bazel ، هذا هو الدليل الذي توجد فيه جميع ملفات المصدر الخاصة بك. لتعيين مساحة العمل هذه ، تحتاج إلى إنشاء ملف فارغ باسم WORKSPACE في الدليل الذي نحتاج إليه ، وعادة ما يكون هذا هو دليل src.

الحد الأدنى لوحدة تنظيم الكود في بازل هو حزمة. يتم تعريف الحزمة بواسطة دليل المصدر وملف BUILD خاص يصف كيفية تجميع هذه المصادر.

أضف الحزمة الرئيسية إلى مشروعنا:



في ملف BUILD ، يجب علينا الآن وصف ما نريد بناءه من مشروعنا الرئيسي. بطبيعة الحال ، نحن نريد تجميع ثنائي قابل للتنفيذ ، لذلك سنستخدم قاعدة cc_binary. يدعم Bazel بالفعل C ++ خارج الصندوق ، لذلك هناك بالفعل مجموعة معينة من القواعد لبناء أهداف c ++ ، سنتعرف على الباقي لاحقًا.

أضف قاعدة cc_binary إلى ملف BUILD ، يحتوي على اسم يحتوي على ملف قابل للتنفيذ ومجموعة من المصادر التي سيتم تمريرها إلى المترجم. كل هذا موصوف في ستارلارك ، وهو ثعبان مقطوع.

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

Bazel ، على عكس cmake ، لا يعتمد على الأوامر ، ولكنه يسمح بشكل توضيحي بالتبعيات من خلال القواعد. بشكل أساسي ، تربط القواعد العديد من القطع الأثرية بعملية معينة. باستخدامها ، يقوم bazel بإنشاء رسم بياني للأوامر ، والذي يقوم بعد ذلك بالتخزين المؤقت والتنفيذ. في حالتنا ، يرتبط الملف المصدر main.cc بعملية تجميع ، والنتيجة هي الأداة hello_world - ملف تنفيذي ثنائي.

للحصول على ملفنا التنفيذي الآن ، يجب أن نذهب إلى الدليل مع مساحة العمل ونكتب:

bazel build //main:hello_world

يقبل نظام البناء أمر الإنشاء والطريق إلى هدفنا ، بدءًا من جذر مشروعنا.

سيتم وضع ثنائي الناتج في bazel-bin / main / hello_world.

مثال 2. بناء مع مكتبتك


لحسن الحظ ، لا يحتاج أحد إلى مثل هذه المشاريع البسيطة ، لذا دعنا نرى كيفية إضافة وظائف إلى مشروعنا. أضف مكتبة سيتم بناؤها بشكل منفصل وربطها بمكتبتنا الرئيسية.

دعها تكون Square ، مكتبة توفر وظيفة تربيع لا لبس فيها. تعني إضافة مكتبة جديدة إضافة حزمة جديدة ، فلنطلق عليها أيضًا مربع.



مربع. ح

#ifndef SQUQRE_SQUARE_H_
#define SQUQRE_SQUARE_H_

int Square(int x);

#endif // SQUQRE_SQUARE_H_

مربع
#include "square/square.h"

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

انتبه إلى اتصال ملف الرأس ، أفعل ذلك من خلال المسار من مساحة العمل ، على الرغم من أن الملف موجود في نفس الدليل. تم اعتماد هذا النهج في دليل نمط رمز الكروم ، الموروث من دليل نمط google c ++. تسمح لك هذه الطريقة بفهم مكان اتصال ملف الرأس على الفور. لا تقلق ، سيكون هناك ملف ، سيضيف bazel مسارات للبحث عن ملفات الرأس ، ولكن إذا لم تتبع هذه القاعدة ، فقد لا يتم العثور على ملفات الرأس أثناء بناء bazel.

في ملف BUILD الخاص بمكتبتنا ، نقوم بوصف القاعدة لبناء مكتبات cc_library:

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

نذكر هنا بشكل منفصل ملفات المصدر والرأس ، ونحدد أيضًا مستوى الرؤية بشكل عام. هذا الأخير ضروري حتى نتمكن من الاعتماد على مكتبتنا في أي مكان في مشروعنا.

في main.cc نستخدم مكتبتنا:

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

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

مرة أخرى ، أوجه الانتباه إلى حقيقة أننا قمنا بتضمين ملف رأس المكتبة من خلال المسار من مساحة العمل. هذا ضروري للغاية هنا ، لأن bazel يستخدم حاويات Linux تحت غطاء المحرك لضمان الحد الأدنى من ضيق التجميع ، وبالتالي ، سيتم تحميل ملفات رأس المكتبة المربعة فقط بحيث يتم وضعها من خلال المسار من مساحة العمل.

ونصف التبعية في قاعدة التجميع الرئيسية في المكتبة المربعة.

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

يتم تجميع برنامجنا بالكامل بنفس الطريقة بدون استخدام المكتبة ، سيفهم بازل نفسه ما يعتمد عليه ، وبناء رسم بياني ، وتخزين النتائج مؤقتًا وإعادة بناء ما يحتاج إلى إعادة بنائه فقط.

bazel build //main:hello_world

مثال 3. ربط الاختبارات


كيف تعيش بدون اختبارات؟ لا يمكن! للاتصال بـ bazel GTest ، الذي يدعم بالفعل التجمع مع bazel ، تحتاج إلى إضافة تبعية خارجية. يتم ذلك في ملف 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"
)

تمامًا مثل محبو موسيقى الجاز ، قاموا بتوصيل قاعدة git_repository وأخبروا بازل بالنسخة التي سيتم تنزيلها.

بعد ذلك ، نقوم بإنشاء حزمة منفصلة لاختبارات الاختبار وإضافة اختبارات إلى مكتبتنا فيها:

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

الآن حان دور تحديد قاعدة للاختبارات.

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

أضفنا تبعيات على مكتبتنا وعلى gtest_main بحيث توفر لنا مكتبة gtest نفسها تطبيق قاذفة.

يتم تشغيل الاختبارات باستخدام الأمر:

bazel test //test:unittests

سيقوم Bazel بتنزيل وبناء GTest نفسه ، وربط كل ما هو مطلوب للاختبارات وتشغيل الاختبارات بأنفسهم.

أذكر أن بازل يعرف أيضًا كيفية القيام بتغطية الرمز:

bazel coverage //test:unittests

وإذا كنت بحاجة إلى تصحيح الاختبارات ، فيمكنك تجميع كل شيء في وضع التصحيح بأحرف مثل هذا:

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

مثال 4. توصيل مكتبات أخرى لا تعرف كيف تستعمل bazel


بالطبع ، العالم ليس مبنياً على بازل فقط ، لذا يجب أن تكون قادرًا على ربط مكتبات أخرى أيضًا. في الآونة الأخيرة ، في مشروعي ، كنت بحاجة إلى مكتبة لتحليل حجج سطر الأوامر. حسنًا ، لا تكتب إلي في 2k20 مكتبتك الخاصة وأن تشتت انتباهك عن العمل الرئيسي. لا أرغب حقًا في استخدام أي تدابير نصفية ، مثل getops ، بالإضافة إلى زيادة السحب في مشروعي.

ليس للإعلان ، سنقوم بتوصيل مكتبة CLI11 ، التي لا تستخدم أكثر من معيار st ++ C ++ 11 وتوفر واجهة أكثر أو أقل ملاءمة.

المكتبة هي رأس فقط ، مما يجعل ربطها سهلاً بشكل خاص.

ربط التبعية الخارجية في 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...",
)

نضيف دليل الطرف الثالث ونضيف حزمة CLI11 لتسهيل بناء التبعيات على هذه المكتبة:

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

سيبحث Bazel افتراضيًا عن ملف المكتبة عن طريق المسار / خارجي / CLI11 حتى نغير المسارات قليلاً لربطه عبر 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;
}

اعتمادًا على main ، أضف "// third_party / CLI11: CLI11" وسيبدأ كل شيء في العمل.
أنا لا أعرف عنك ، لكن ربط مكتبة غير مألوفة واستخدامها في مشروع c ++ في هذا النموذج يسعدني.

نعم ، مع مكتبة الرأس فقط ، ستقول أن كل شيء بسيط ، ولكن مع مكتبة غير رأسية فقط لم يتم إنشاؤها بعد باستخدام كل شيء ، فإن كل شيء بسيط للغاية. يمكنك ببساطة تنزيله عبر http_archive أو git_repository وإضافة ملف BUILD خارجي إليه في دليل الطرف الثالث ، حيث تصف كيفية إنشاء مكتبتك. يدعم Bazel استدعاء أي cmd وحتى استدعاء cmake ، من خلال قاعدة cmake_external.

مثال 5. البرامج النصية والأتمتة


من يحتاج إلى مشروع في c ++ عارية في 2k20 بدون سكربتات للأتمتة؟ عادةً ما تكون مثل هذه النصوص البرمجية مطلوبة لإجراء اختبارات الأداء أو لنشر التحف في مكان ما إلى CI. حسنًا ، عادةً ما تكون مكتوبة بلغة الثعبان.

لهذا ، فإن bazel مناسب أيضًا ، حيث يمكن استخدامه في جميع اللغات الشائعة تقريبًا وهو مصمم فقط لجمع هذه solyanka من لغات البرمجة المختلفة التي غالبًا ما توجد في المشاريع الحقيقية.

دعنا نربط نص بايثون الذي سيدير ​​برنامجنا الرئيسي.

أضف حزمة اختبارات الأداء:

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

باعتماد البيانات ، أضف التبعية الثنائية 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))

من أجل إجراء اختباراتنا ، نكتب ببساطة:
bazel run //perf-tests:perf_tests

الخلاصة وما لم يلمس


نظرنا بإيجاز إلى bazel وميزاته الرئيسية لتجميع الملفات والمكتبات القابلة للتنفيذ ، سواء الخاصة أو الخارجية. لذوقي ، اتضح بإيجاز وبسرعة كبيرة. لا حاجة للمعاناة والبحث عن بعض دروس المكياج للقيام ببعض الأشياء التافهة وتنظيف CmakeCache.

إذا كنت مهتمًا ، فلا يزال هناك الكثير المتبقي: ربط مخازن البروتوكول ، والمعقمات ، وإنشاء سلسلة أدوات لتجميع المنصات / الهياكل المختلفة.

شكرا للقراءة ، وآمل أن أكون مفيدة لك.

All Articles