সরাসরি পর্বে যান
পর্ব ৪৭ · ৫৫-এর মধ্যে · মডিউল ৭

CMake — বড় C++ প্রজেক্ট বিল্ড করার রেসিপি

CMake — A Recipe for Building Large C++ Projects

Taught in Bangla, this lesson explains building C++ projects with CMake: what a build system generator does, a first CMakeLists.txt, configure, generate and build, modern target-based CMake with PUBLIC, PRIVATE and INTERFACE, project layout, build types, sanitizers and presets, and external libraries via FetchContent and vcpkg.

এই পর্বে যা শিখবেন

  • Build system কোন সমস্যা সমাধান করে, আর “শুধু যা বদলেছে তা-ই আবার build” কীভাবে কাজ করে
  • CMake (generator) আর Ninja, Make, MSBuild (build টুল)-এর পার্থক্য
  • একটি ছোট CMakeLists.txt, আর configure → generate → build-এর তিন ধাপ
  • Target, static ও shared library, আর PUBLIC/PRIVATE/INTERFACE-এর মানে — একটি দোকানের প্রজেক্ট দিয়ে
  • Debug/Release, warning, sanitizer, বাইরের library, presets, টেস্ট — আর AI-এর লেখা পুরোনো ধাঁচের CMake চেনা

১ · তিন ফাইল থেকে তিনশো — হাতের কমান্ড কেন আর চলে না

পর্ব ০৩-এ আপনি toolchain-এর পাইপলাইন দেখেছেন: প্রতিটি .cpp ফাইল আলাদা করে compile হয়ে একটি object file হয়, তারপর linker সবগুলো জুড়ে একটি executable বানায়। পর্ব ১১-এ রাকিবের দোকানের প্রজেক্টটি header আর .cpp-তে ভাগ করেছিলেন। তখন কাজ চালাতে একটিমাত্র লাইনই যথেষ্ট ছিল:

g++ -std=c++23 -Wall -Wextra main.cpp money.cpp -o shop

এখন কল্পনা করুন রাকিবের দোকান বড় হয়েছে। এখন তিনটি শাখা, অনলাইন অর্ডার, bKash-এ পেমেন্ট, মাসিক হিসাবের রিপোর্ট। কোডে ৩০০টি .cpp ফাইল, তিনটি বাইরের library, একটি টেস্ট প্রোগ্রাম। দলের কেউ Windows-এ MSVC চালান, কেউ Linux-এ GCC. আর প্রতিটি প্রোগ্রাম দুইভাবে বানাতে হয় — debug করার জন্য একটি, গ্রাহকের জন্য দ্রুতগতির আরেকটি। এখন হাতে কমান্ড লেখার চারটি সমস্যা একসাথে দেখা দেয়:

১. কমান্ড অসম্ভব লম্বা

৩০০টি ফাইলের নাম, প্রতিটি library-র header-এর ঠিকানা (-I), link করার নাম (-l), সব flag — এক লাইনে লেখা যায় না, আর একটি ভুলেই linker error.

২. সবকিছু বারবার compile

একটি ফাইলে একটি লাইন বদলালেও পুরো ৩০০টি ফাইল আবার compile হয়। বড় C++ প্রজেক্টে সেটা কয়েক মিনিট থেকে কয়েক ঘণ্টা।

৩. প্রতিটি compiler-এর আলাদা ভাষা

GCC-তে -Wall -O2, MSVC-তে /W4 /O2. একই প্রজেক্টের জন্য দুই রকম কমান্ড আলাদা করে রক্ষণাবেক্ষণ করতে হয়।

৪. Debug আর Release

একই কোড, ভিন্ন flag (-g বনাম -O2), ভিন্ন ফোল্ডার। হাতে করলে দুটো গুলিয়ে যায়, আর “আমার মেশিনে চলে” সমস্যা শুরু হয়।

এর সমাধানের নাম build system. এর মূল ধারণা খুব পুরোনো আর খুব সরল: প্রজেক্টকে একটি dependency graph হিসেবে দেখা। প্রতিটি তৈরি ফাইল কোন কোন ফাইল থেকে আসে — pricing.o আসে pricing.cpp আর সে যে header include করে সেগুলো থেকে; shop আসে সব .o থেকে। Build টুল প্রতিটি তৈরি ফাইলের সময়-চিহ্ন (timestamp) তার উৎসের সাথে মেলায়। উৎস যদি তৈরি ফাইলের চেয়ে নতুন হয়, শুধু তখনই আবার বানায়। বাকি সব আগের মতোই থাকে। এর নাম incremental build.

একটি গার্মেন্টস কারখানার কথা ভাবুন। একটি শার্ট বানাতে লাগে কাটা কাপড়, কলার, বোতাম আর সেলাই। ক্রেতা শুধু বোতামের রং বদলাতে বললেন। ভালো কারখানা পুরো কাপড় আবার কাটে না — শুধু বোতাম লাগানোর ধাপটি আবার করে। কিন্তু এজন্য ফ্লোর ম্যানেজারের কাছে একটি উৎপাদন-নকশা থাকা চাই: কোন অংশ কোন অংশের ওপর নির্ভর করে। Build system হলো সেই নকশা, আর তাকে মেনে কাজ করা মেশিন।

এই ধারণার প্রথম জনপ্রিয় টুল make (১৯৭৬, Bell Labs). make একটি Makefile পড়ে, যেখানে লেখা থাকে “এই ফাইল বানাতে ওই ফাইলগুলো লাগে, আর এই কমান্ড চালাতে হবে।” C কোর্সে এটি হাতে লিখে দেখেছেন। কিন্তু Makefile হাতে লেখারও সীমা আছে: header-এর নির্ভরতা নিজে মনে রাখতে হয়, Windows-এর MSVC সাধারণ Makefile বোঝে না, আর ৩০০ ফাইলে Makefile নিজেই একটি জটিল প্রোগ্রাম হয়ে যায়। এখানেই CMake-এর জায়গা।

C জানলে

Makefile-এর নিয়ম (target, নির্ভরতা, কমান্ড), static library (ar দিয়ে .a) আর header ভাগ করা — সব C-র মতোই। C++-এ build বেশি ভারী, কারণ template আর বড় header প্রতিটি ফাইলের compile সময় বাড়ায়, তাই incremental build আর ঠিক build টুল আরও জরুরি। CMake দুই ভাষাতেই একই; শুধু project(... LANGUAGES C)-এর জায়গায় CXX. হাতে লেখা Makefile ধাপে ধাপে দেখতে C কোর্সের মডুলার প্রোগ্রাম — header, Makefile, library পর্বটি পড়ুন।

২ · CMake আসলে কী — রেসিপি লেখক, রাঁধুনি নয়

নতুনরা প্রায়ই ভাবেন CMake একটি compiler বা একটি build টুল। দুটোর কোনোটিই নয়। CMake হলো একটি build system generator: আপনি একবার একটি প্ল্যাটফর্ম-নিরপেক্ষ বর্ণনা লেখেন — “এই নামের একটি library আছে, এই ফাইলগুলো নিয়ে; এই প্রোগ্রাম সেটি ব্যবহার করে; C++23 লাগবে।” CMake সেটা পড়ে আপনার মেশিনের জন্য ঠিক build ফাইল লিখে দেয়। তারপর সেই ফাইল দেখে অন্য একটি টুল আসল compile আর link চালায়।

বিয়েবাড়িতে ৫০০ জনের বিরিয়ানি। একজন প্রধান বাবুর্চি রেসিপি আর কাজের তালিকা লেখেন: কত কেজি চাল, কোন হাঁড়িতে কী, কোনটা আগে। তিনি নিজে হাঁড়ি নাড়েন না। ঢাকার রান্নাঘরে গ্যাসের চুলা, গ্রামের বাড়িতে লাকড়ির চুলা — তালিকাটা একটু বদলে দেন, যাতে সেখানকার রাঁধুনিরা বুঝতে পারেন। CMake হলো প্রধান বাবুর্চি; Ninja, Make, MSBuild হলো রাঁধুনি; compiler হলো চুলা।

CMake যে ধরনের build ফাইল লেখে, তাকে বলে generator. প্রচলিতগুলো:

Generatorযে ফাইল লেখেযে টুল চালায়কোথায় বেশি দেখা যায়
Ninjabuild.ninjaNinjaLinux, Mac, Windows — দ্রুত, আজকের প্রিয় পছন্দ
Unix MakefilesMakefilemakeLinux ও Mac-এ অনেক সময় ডিফল্ট
Visual Studio 17 2022.sln, .vcxprojMSBuildWindows-এ MSVC দিয়ে
Xcode.xcodeprojxcodebuildMac ও iOS

ফলে একই CMakeLists.txt দিয়ে রাকিবের দলের Windows-ব্যবহারকারী Visual Studio-তে প্রজেক্ট খোলেন, আর Linux-ব্যবহারকারী terminal-এ Ninja চালান। কেউ কারও build ফাইল হাতে লেখেন না। আজকের IDE-গুলো — Visual Studio, CLion, VS Code-এর CMake Tools, Qt Creator — সরাসরি CMakeLists.txt বোঝে। এজন্যই CMake C++ জগতে প্রায় মানদণ্ড হয়ে গেছে, যদিও standard-এ এর কোনো উল্লেখ নেই।

CMake-এর জন্ম Kitware নামের একটি মার্কিন প্রতিষ্ঠানে, ১৯৯৯-২০০০ সালের দিকে। তারা তখন ITK (Insight Toolkit) নামে একটি চিকিৎসা-ছবি বিশ্লেষণের C++ library বানাচ্ছিল, যার অর্থায়ন করেছিল যুক্তরাষ্ট্রের National Library of Medicine. লাইব্রেরিটিকে Windows, Linux আর নানা Unix-এ, নানা compiler-এ build করতে হতো — হাতে আলাদা আলাদা build ফাইল রাখা অসম্ভব হয়ে উঠছিল। Bill Hoffman-এর নেতৃত্বে লেখা CMake সেই সমস্যার সমাধান। নামটি এসেছে “cross-platform make” থেকে। পরে KDE-র মতো বড় প্রজেক্ট CMake-এ চলে আসায় এটি দ্রুত ছড়িয়ে পড়ে।

খেয়াল করুন, CMake একমাত্র পথ নয়। Google-এর Bazel, Meson, xmake, build2 — সবই C++ build করে, আর কিছু ক্ষেত্রে CMake-এর চেয়ে ভালোও। কিন্তু বাইরের বেশিরভাগ C++ library CMake দিয়ে আসে, আর চাকরির বিজ্ঞাপনে “CMake” শব্দটি সবচেয়ে বেশি দেখা যায়। তাই এই কোর্সে CMake.

৩ · প্রথম CMakeLists.txt — চার লাইনের রেসিপি

CMake-এর রেসিপি থাকে প্রজেক্টের মূল ফোল্ডারে, CMakeLists.txt নামের একটি ফাইলে (নামের বানান হুবহু এমন)। পর্ব ১১-এর তিন ফাইলের দোকানের প্রজেক্টের জন্য এর সবচেয়ে ছোট রূপ:

cmake_minimum_required(VERSION 3.25)
project(shop VERSION 1.0 LANGUAGES CXX)

add_executable(shop main.cpp money.cpp)
target_compile_features(shop PRIVATE cxx_std_23)

লাইন ১: cmake_minimum_required — এই রেসিপি কোন CMake version-এর নিয়ম ধরে লেখা। শুধু “এর চেয়ে পুরোনো CMake চলবে না” নয়; CMake এই সংখ্যা দেখে তার নানা আচরণ ঠিক করে, যাকে বলে policy. তাই সংখ্যাটি হওয়া উচিত আপনি আসলে যে version দিয়ে পরীক্ষা করেছেন তার কাছাকাছি — C++23-এর জন্য অন্তত ৩.২০.

লাইন ২: project — প্রজেক্টের নাম, version আর ভাষা। LANGUAGES CXX মানে C++. এই লাইনেই CMake আপনার মেশিনে C++ compiler খুঁজে বের করে।

লাইন ৪: add_executable — “shop নামের একটি প্রোগ্রাম বানাও, এই source ফাইলগুলো দিয়ে।” Header ফাইল তালিকায় না দিলেও চলে; CMake আর compiler নিজেই include-এর নির্ভরতা খুঁজে নেয়।

লাইন ৫: target_compile_features — “এই প্রোগ্রামের জন্য অন্তত C++23 লাগবে।” CMake প্রতিটি compiler-এর জন্য ঠিক flag বসায়: GCC-তে -std=c++23 বা -std=gnu++23, MSVC-তে তার নিজের flag. আপনাকে আর compiler-ভেদে flag মনে রাখতে হয় না।

অনেক টিউটোরিয়ালে লাইন ৫-এর বদলে পুরো প্রজেক্টের জন্য একটি variable দেখবেন:

set(CMAKE_CXX_STANDARD 23)           # সব target-এর ডিফল্ট standard
set(CMAKE_CXX_STANDARD_REQUIRED ON)  # না পেলে থামবে, পুরোনোতে নামবে না
set(CMAKE_CXX_EXTENSIONS OFF)        # gnu++23 নয়, খাঁটি -std=c++23

দুটোই চলে। একটি ছোট অ্যাপের জন্য এই তিন লাইন সুবিধাজনক। কিন্তু library বানালে target_compile_features(... PUBLIC cxx_std_23) ভালো, কারণ এটা ব্যবহারকারীদেরও জানিয়ে দেয় “আমার header বুঝতে C++23 লাগবে” — অংশ ৫-এ এর মানে পরিষ্কার হবে। CMAKE_CXX_EXTENSIONS OFF রাখলে GCC-র নিজস্ব extension বন্ধ থাকে, ফলে কোড অন্য compiler-এ নিয়ে গেলে কম চমক আসে।

তিন ধাপ: configure, generate, build

রেসিপি লেখা হলো। এবার প্রজেক্টের মূল ফোল্ডারে terminal খুলে দুটি কমান্ড:

cmake -S . -B build -G Ninja    # source: এই ফোল্ডার, build: build/ ফোল্ডার
cmake --build build             # compile + link (ভেতরে Ninja চালায়)
./build/shop                    # Windows-এ: build\shop.exe

প্রথম কমান্ডটি আসলে দুটি কাজ করে — configure আর generate. দ্বিতীয়টি build. -G Ninja না দিলে CMake নিজের ডিফল্ট generator নেয় (Linux-এ সাধারণত Makefile, Windows-এ Visual Studio). নিচের ডায়াগ্রামে দেখুন প্রতিটি ধাপে build/ ফোল্ডারে কী কী ফাইল জন্ম নেয়:

  1. রেসিপিCMakeLists.txtআপনার লেখা
  2. Configurecmake -S . -B buildcompiler খোঁজা
  3. Generatebuild.ninjabuild ফাইল লেখা
  4. Buildcmake --build buildcompile + link
  5. নতুন করেrm -rf buildsource অক্ষত
shop/শুধু আপনার ফাইল
shop/
├── CMakeLists.txt
├── main.cpp
├── money.h
└── money.cpp          # build/ এখনো নেই
$ cmake -S . -B build -G Ninjaconfigure
-- The CXX compiler identification is GNU 14.2.0
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Configuring done
build/
├── CMakeCache.txt     # মনে রাখা সিদ্ধান্ত
└── CMakeFiles/        # compiler পরীক্ষার ফল
(একই কমান্ডের শেষ অংশ)generate
-- Generating done
-- Build files have been written to: /home/rakib/shop/build
build/
├── CMakeCache.txt
├── CMakeFiles/
├── build.ninja        # Ninja-র জন্য কাজের তালিকা
└── cmake_install.cmake
$ cmake --build buildbuild
[1/3] Building CXX object CMakeFiles/shop.dir/main.cpp.o
[2/3] Building CXX object CMakeFiles/shop.dir/money.cpp.o
[3/3] Linking CXX executable shop
build/
├── CMakeFiles/shop.dir/main.cpp.o, money.cpp.o
└── shop               # Windows-এ shop.exe
$ rm -rf buildout-of-source-এর সুবিধা
shop/
├── CMakeLists.txt
├── main.cpp
├── money.h
└── money.cpp          # একটুও বদলায়নি
# আবার শুরু: cmake -S . -B build
# চাইলে পাশাপাশি: build-debug/ আর build-release/
  1. শুরু। আপনার ফোল্ডারে শুধু source আর CMakeLists.txt. এখানে কোনো Makefile বা build.ninja হাতে লিখবেন না — এগুলো CMake-এর কাজ।
  2. Configure. CMake রেসিপিটি ওপর থেকে নিচে চালায়। project() লাইনে এসে সে compiler খোঁজে, একটি ছোট পরীক্ষামূলক প্রোগ্রাম compile করে দেখে compiler কাজ করে কি না, আর কী কী সমর্থন করে। সব সিদ্ধান্ত লিখে রাখে CMakeCache.txt-এ, যাতে পরের বার আবার খুঁজতে না হয়।
  3. Generate. সব target আর নিয়ম জানা হয়ে গেলে CMake বাছাই করা generator-এর ভাষায় build ফাইল লেখে — এখানে build.ninja. এতে প্রতিটি .cpp-এর পুরো compile কমান্ড আর কোনটি কিসের ওপর নির্ভর করে, সব লেখা থাকে। “Configuring done” আর “Generating done” দুটো লাইনই এই এক কমান্ড থেকে আসে।
  4. Build. cmake --build build নিজে compile করে না; সে build/-এ যে টুলের ফাইল আছে (এখানে Ninja) সেটি চালায়। Ninja প্রতিটি .cpp আলাদা compile করে .o বানায় — একাধিক CPU core-এ একসাথে — তারপর link করে। এই কমান্ড সব generator-এ একই, তাই script-এ নির্ভয়ে লেখা যায়।
  5. Out-of-source build. সব তৈরি জিনিস build/-এর ভেতরে, source ফোল্ডার পরিষ্কার। কিছু গোলমাল হলে build/ মুছে আবার শুরু করুন — কিছুই হারায় না। এর নাম out-of-source build, আর আধুনিক CMake-এ এটিই প্রস্তাবিত উপায়। একই source থেকে পাশাপাশি কয়েকটি build ফোল্ডারও রাখা যায়।

প্রজেক্টের ফোল্ডারেই cmake . চালাবেন না। তখন CMakeCache.txt, CMakeFiles/, Makefile সব source-এর মাঝে ছড়িয়ে পড়ে — এর নাম in-source build. Git-এ ভুল ফাইল ঢুকে যায়, আর “পরিষ্কার করে আবার শুরু” করার সহজ উপায় থাকে না। পুরোনো কোনো ফোল্ডারে ভুল করে একবার চালিয়ে ফেললে পরে -B build দিলেও পুরোনো cache গোলমাল করতে পারে; তখন সেই CMakeCache.txt আর CMakeFiles/ মুছে ফেলুন।

৪ · কোনটি আবার build হবে — dependency graph-এর হিসাব

অংশ ১-এর দ্বিতীয় সমস্যা ছিল “একটি লাইন বদলালে সব আবার compile.” এখন দেখা যাক Ninja আর Make এটি কীভাবে এড়ায়। এজন্য দুটি তথ্য লাগে। প্রথমটি CMake দেয়: কোন .o কোন .cpp থেকে, কোন library কোন .o থেকে, কোন প্রোগ্রাম কোন library থেকে। দ্বিতীয়টি compiler দেয়: প্রতিটি .cpp compile করার সময় সে একটি ছোট তালিকা লিখে রাখে, ওই ফাইল কোন কোন header পড়েছে (GCC-তে -MD flag, যা CMake নিজেই বসায়)। দুটো মিলিয়ে build টুল পুরো graph জানে।

এবার দোকানের প্রজেক্টটি একটু বড় রূপে ভাবুন: inventory.cpp আর pricing.cpp মিলে একটি library, আর main.cpp সেই library ব্যবহার করে একটি প্রোগ্রাম। pricing.h header-টি pricing.cpp আর main.cpp দুটোই include করে, inventory.cpp করে না। নিচে পাঁচটি ঘটনায় দেখুন কী কী আবার তৈরি হয় (লাল দাগ = এই build-এ আবার বানানো হলো):

CMakeLists.txtরেসিপি
pricing.hpricing.cpp, main.cpp পড়ে
inventory.cpp
pricing.cpp
main.cpp
inventory.cpp.o
pricing.cpp.o
main.cpp.o
build.ninjaCMake আবার লেখে
libshop_core.alibrary
shop_cliপ্রোগ্রাম
$ cmake --build buildপ্রথমবার: সব
[1/5] Building CXX object CMakeFiles/shop_core.dir/src/inventory.cpp.o
[2/5] Building CXX object CMakeFiles/shop_core.dir/src/pricing.cpp.o
[3/5] Building CXX object CMakeFiles/shop_cli.dir/apps/main.cpp.o
[4/5] Linking CXX static library libshop_core.a
[5/5] Linking CXX executable shop_cli
$ cmake --build buildকিছুই বদলায়নি
ninja: no work to do.
pricing.cpp-এ VAT-এর হিসাব বদলালেন৩টি ধাপ
[1/3] Building CXX object CMakeFiles/shop_core.dir/src/pricing.cpp.o
[2/3] Linking CXX static library libshop_core.a
[3/3] Linking CXX executable shop_cli
# inventory.cpp আর main.cpp ছোঁয়াই হয়নি
pricing.h-এ একটি নতুন ফাংশন ঘোষণা৪টি ধাপ
[1/4] Building CXX object CMakeFiles/shop_core.dir/src/pricing.cpp.o
[2/4] Building CXX object CMakeFiles/shop_cli.dir/apps/main.cpp.o
[3/4] Linking CXX static library libshop_core.a
[4/4] Linking CXX executable shop_cli
CMakeLists.txt-এ একটি flag যোগ করলেনআগে configure
[0/1] Re-running CMake...
-- Configuring done
-- Generating done
[1/5] Building CXX object ...inventory.cpp.o
# ...flag বদলেছে, তাই যাদের কমান্ড বদলেছে সবাই আবার
  1. প্রথম build. কিছুই তৈরি নেই, তাই সব। লক্ষ করুন ক্রম: দুটি library-ফাইল আর main.cpp একে অন্যের ওপর নির্ভর করে না, তাই Ninja তিনটিকে একসাথে আলাদা CPU core-এ compile করতে পারে। Link শুধু সব .o তৈরি হওয়ার পরে।
  2. কিছু না বদলে আবার build. Ninja প্রতিটি ফাইলের timestamp দেখে, সব তৈরি ফাইল তার উৎসের চেয়ে নতুন — কাজ শেষ। বড় প্রজেক্টে এই “কিছু না করা”-টাও কত দ্রুত হয়, সেটাই Ninja-র বড় শক্তি (ভাবনার প্রশ্ন ০৪)।
  3. একটি .cpp বদলালেন। শুধু pricing.cpp.o আবার compile, তারপর library আবার জোড়া, তারপর প্রোগ্রাম আবার link. বাকি দুটি ফাইলের compile পুরো বাদ। ৩০০ ফাইলের প্রজেক্টে এটাই কয়েক মিনিট আর কয়েক সেকেন্ডের পার্থক্য।
  4. একটি header বদলালেন। যে যে .cpp pricing.h পড়ে (সরাসরি বা অন্য header-এর মাধ্যমে), সবাই আবার compile. inventory.cpp এটি পড়ে না, তাই সে বাঁচে। এজন্যই পর্ব ১১-এর পরামর্শ: header ছোট রাখুন, অদরকারি #include নয়, যেখানে চলে forward declaration — বহুল-ব্যবহৃত একটি header বদলানো মানে অর্ধেক প্রজেক্ট আবার compile.
  5. রেসিপি বদলালেন। build.ninja নিজেই CMakeLists.txt-এর ওপর নির্ভরশীল। তাই Ninja প্রথমে CMake-কে আবার চালায় (আপনাকে হাতে cmake -S . -B build দিতে হয় না), নতুন build.ninja আসে, তারপর যে ফাইলগুলোর compile কমান্ড বদলেছে সেগুলো আবার compile হয়।
মূল ধারণা

Build টুলের কাছে প্রজেক্ট মানে একটি graph: প্রতিটি তৈরি ফাইল আর তার উৎসগুলো। কোনো উৎস বদলালে শুধু সেখান থেকে নিচের দিকে যারা আছে, তারাই আবার তৈরি হয়। আপনার কাজ সেই graph ঠিক রাখা — CMake-কে সঠিক target আর নির্ভরতা জানানো; বাকি হিসাব টুলের।

“কোনটি আবার বানাতে হবে” প্রশ্নটি দেখতে সহজ, কিন্তু এখানেই সবচেয়ে বিরক্তিকর build-বাগ লুকিয়ে থাকে: নির্ভরতা ভুল জানালে টুল এমন ফাইল আবার বানায় না যেটা বানানো দরকার ছিল। তখন পুরোনো .o নতুন header-এর সাথে link হয়, আর অদ্ভুত crash আসে যা “clean build”-এ উধাও। CMake আর compiler-এর স্বয়ংক্রিয় header-তালিকা এই ঝুঁকি প্রায় শূন্যে নামিয়ে আনে — যদি আপনি হাতের কৌশলে সেটা না ভাঙেন (অংশ ৯)।

৫ · Modern CMake — target আর তার property

CMake ২০০০ সাল থেকে আছে, আর তার লেখার ধাঁচ সময়ের সাথে অনেক বদলেছে। পুরোনো ধাঁচে সবকিছু ছিল global variable আর global কমান্ড: “সবার জন্য এই include ফোল্ডার, সবার জন্য এই flag.” প্রায় ২০১৪ সালের CMake ৩.০ থেকে যে ধাঁচ চালু হয়েছে, তাকে বলে modern CMake. এর মূল কথা একটি: সবকিছু একটি target-এর গায়ে লেখা। Target মানে CMake যা বানায় — একটি প্রোগ্রাম বা একটি library. প্রতিটি target-এর নিজের property থাকে: কোন source, কোন include ফোল্ডার, কোন flag, কোন library-র সাথে link.

তিন রকম library

add_library(shop_core STATIC src/inventory.cpp src/pricing.cpp)
add_library(shop_report SHARED src/report.cpp)
add_library(shop_money INTERFACE)       # শুধু header, কোনো .cpp নেই

STATIC

Static library — Linux-এ libshop_core.a, Windows-এ shop_core.lib. Link-এর সময় এর দরকারি কোড প্রোগ্রামের ভেতরে কপি হয়ে যায়। প্রোগ্রাম একা চলে, বাড়তি ফাইল লাগে না।

সরল, সবচেয়ে প্রচলিত

SHARED

Shared library — Linux-এ .so, Windows-এ .dll, Mac-এ .dylib. প্রোগ্রাম চলার সময় আলাদা ফাইল থেকে লোড হয়; কয়েকটি প্রোগ্রাম একটি কপি ভাগ করে নেয়, আর library একা আপডেট করা যায়। বিনিময়ে ABI নিয়ে সতর্কতা, আর Windows-এ কোন ফাংশন বাইরে দেখানো হবে (export) তা আলাদা করে জানাতে হয়।

plugin, বড় SDK

INTERFACE

Interface library — নিজে কিছু compile হয় না। শুধু “আমাকে ব্যবহার করলে এই include ফোল্ডার আর এই নিয়ম পাবেন” — header-only library-র জন্য ঠিক এটাই।

header-only

add_library-এ ধরন না লিখলে CMake BUILD_SHARED_LIBS নামের একটি variable দেখে ঠিক করে — ব্যবহারকারী configure-এর সময় -DBUILD_SHARED_LIBS=ON দিয়ে বদলাতে পারেন। শেখার সময় STATIC লিখে দেওয়াই সবচেয়ে সহজ।

PUBLIC, PRIVATE, INTERFACE — “শুধু আমার” আর “আমার ব্যবহারকারীরও”

এবার modern CMake-এর সবচেয়ে গুরুত্বপূর্ণ ধারণা। প্রতিটি target_* কমান্ডে একটি শব্দ বসে, যা বলে এই জিনিসটি কার লাগবে:

target_include_directories(shop_core PUBLIC include)
target_link_libraries(shop_core
    PUBLIC  shop_money    # shop_core-এর header shop_money-র header পড়ে
    PRIVATE fmt::fmt)     # fmt শুধু shop_core-এর .cpp-র ভেতরে
target_link_libraries(shop_cli PRIVATE shop_core)

ধরুন একটি কারখানা মোবাইল ফোন বানায়। ফোনে USB-C পোর্ট আছে — তাই ক্রেতারও একটি USB-C তার লাগবে। এটি PUBLIC: “আমার লাগে, আর আমাকে যে ব্যবহার করবে তারও লাগবে।” কারখানার সোল্ডারিং মেশিন শুধু কারখানার ভেতরে লাগে, ক্রেতার কোনোদিন জানারও দরকার নেই — এটি PRIVATE. আর ফোনের সাথে দেওয়া “ব্যবহারবিধি” কারখানার নিজের কোনো কাজে আসে না, শুধু ক্রেতার জন্য — এটি INTERFACE.

যে জিনিসগুলো এভাবে ব্যবহারকারীর দিকে ছড়ায় — include ফোল্ডার, compile flag, C++ standard, link করার library — CMake-এর ভাষায় সেগুলো usage requirements. আর A যদি B-কে PUBLIC-ভাবে ব্যবহার করে, B যদি C-কে PUBLIC-ভাবে, তাহলে A-র ব্যবহারকারীও C পায় — এর নাম transitive dependency. সিদ্ধান্তের সহজ নিয়ম:

  • B-এর কোনো header যদি আপনার public header-এ #include হয়, বা আপনার public ফাংশনের টাইপে B আসে → PUBLIC.
  • B শুধু আপনার .cpp ফাইলের ভেতরে ব্যবহার হয় → PRIVATE.
  • আপনার নিজের কিছু compile হয় না, শুধু ব্যবহারকারীর লাগে (header-only) → INTERFACE.
  1. shop_moneyINTERFACEশুধু header
  2. shop_coreSTATICPUBLIC money, PRIVATE fmt
  3. shop_cliexecutablePRIVATE shop_core
  4. ভুল হলেNo such filePRIVATE-এর জায়গায় PUBLIC লাগত
shop_moneyনিজে compile হয় না
add_library(shop_money INTERFACE)
target_include_directories(shop_money INTERFACE money/include)
target_compile_features(shop_money INTERFACE cxx_std_23)
# ব্যবহারকারী পাবে: -I money/include, C++23
shop_core নিজে যা দেখেcompile-এর সময়
-I include              # নিজের PUBLIC
-I money/include        # shop_money থেকে
-I .../fmt/include      # fmt (PRIVATE, তবু নিজে পায়)
-std=gnu++23
ব্যবহারকারীকে দেবে: include, money/include, C++23
দেবে না: fmt-এর include
shop_cli যা পায়শিকল বেয়ে
compile:  -I include  -I money/include  -std=gnu++23
          # fmt-এর include নেই — দরকারও নেই
link:     libshop_core.a  libfmt.a
          # static library-র PRIVATE নির্ভরতাও link-এ লাগে,
          # CMake নিজেই যোগ করে
include/shop/pricing.h-এ #include <fmt/format.h>কিন্তু fmt PRIVATE
apps/main.cpp:1:
include/shop/pricing.h:3:10: fatal error: fmt/format.h:
    No such file or directory
# সমাধান ১: target_link_libraries(shop_core PUBLIC fmt::fmt)
# সমাধান ২ (ভালো): header থেকে fmt সরিয়ে .cpp-তে নিন
  1. INTERFACE library. shop_money-তে শুধু Taka টাইপ আর কিছু inline ফাংশন (পর্ব ১১-এর money.h). Compile করার কিছু নেই, তাই সব property INTERFACE — নিজের জন্য নয়, ব্যবহারকারীর জন্য।
  2. shop_core. এর public header-এ Taka টাইপ আসে, তাই shop_money PUBLIC. কিন্তু সুন্দর করে টাকা ছাপানোর fmt library শুধু pricing.cpp-এর ভেতরে লাগে, তাই PRIVATE. নিজে compile হওয়ার সময় shop_core সবকিছু দেখে — PRIVATE মানে “শুধু আমি”, “আমিও না” নয়।
  3. shop_cli. একটি লাইনই লিখেছেন — PRIVATE shop_core — অথচ সে include/, money/include আর C++23 সব পেয়ে গেল। এটাই usage requirements-এর জাদু: প্রতিটি library নিজের প্রয়োজন নিজে ঘোষণা করে, ব্যবহারকারীকে মনে রাখতে হয় না। আর একটি সূক্ষ্ম কথা: shop_core static হওয়ায় তার ভেতরের কোড fmt-এর ফাংশন ডাকে, তাই link-এর সময় fmt লাগবেই — CMake এটা নিজেই link-লাইনে যোগ করে, কিন্তু fmt-এর header shop_cli-কে দেখায় না।
  4. ভুল PRIVATE. কেউ pricing.h-এ #include <fmt/format.h> যোগ করলেন, কিন্তু fmt এখনো PRIVATE. shop_core নিজে ঠিকঠাক build হয়, ভুল ধরা পড়ে শুধু ব্যবহারকারীর ফাইলে — যা আরও বিভ্রান্তিকর। নিয়ম মনে রাখুন: যা আপনার public header-এ ঢোকে, তা PUBLIC. আরও ভালো সমাধান হলো header হালকা রাখা, যাতে নির্ভরতা PRIVATE-ই থাকতে পারে।

উল্টো ভুলটিও ঘটে: সবকিছু PUBLIC করে দেওয়া। তখন প্রতিটি ব্যবহারকারী অদরকারি include ফোল্ডার আর flag পায়, compile ধীর হয়, আর library-র ভেতরের একটি বদল পুরো প্রজেক্টে ছড়িয়ে পড়ে। PRIVATE হলো ডিফল্ট মনোভাব — দরকার প্রমাণ হলে তবেই PUBLIC.

নামে :: কেন? বাইরের library সাধারণত fmt::fmt, GTest::gtest_main-এর মতো নামে target দেয়। CMake নিয়ম করেছে, target_link_libraries-এ ::-যুক্ত নাম পেলে সেটা অবশ্যই একটি target হতে হবে — না পেলে configure-এর সময়েই স্পষ্ট error. :: ছাড়া ভুল বানান লিখলে CMake সেটাকে সাধারণ library-নাম ধরে link-লাইনে -lfmtt বসিয়ে দেয়, আর ভুল ধরা পড়ে অনেক পরে। নিজের library-র জন্যও একটি নাম রাখা ভালো: add_library(shop::core ALIAS shop_core) — এর নাম alias target.

৬ · দোকানের প্রজেক্ট সাজানো — src/, include/, apps/, tests/

এবার সব একসাথে। রাকিবের দোকানের প্রজেক্টের একটি বাস্তবসম্মত বিন্যাস — বেশিরভাগ খোলা C++ প্রজেক্ট মোটামুটি এই ধাঁচ মানে:

shop/
├── CMakeLists.txt          # মূল রেসিপি
├── CMakePresets.json       # প্রচলিত build-এর নাম (অংশ ৭)
├── include/shop/           # public header — ব্যবহারকারী যা দেখবে
│   ├── inventory.h
│   └── pricing.h
├── src/                    # library-র .cpp — ভেতরের কাজ
│   ├── inventory.cpp
│   └── pricing.cpp
├── apps/
│   └── main.cpp            # shop_cli প্রোগ্রাম
└── tests/
    └── pricing_test.cpp    # টেস্ট

এই বিন্যাসের পেছনের যুক্তি: আসল কাজ থাকে library-তে (shop_core), প্রোগ্রাম (shop_cli) শুধু একটি পাতলা খোলস যা ব্যবহারকারীর সাথে কথা বলে। ফলে একই library টেস্ট প্রোগ্রামও ব্যবহার করতে পারে, পরে একটি GUI বা ওয়েব সার্ভারও। include/shop/-এর ভেতরে আরেকটি shop ফোল্ডার কেন? যাতে ব্যবহারকারী লেখেন #include "shop/pricing.h" — অন্য কোনো library-র pricing.h-এর সাথে নাম মিলে যাওয়ার ভয় থাকে না। ফাইলগুলো দেখুন:

cmake_minimum_required(VERSION 3.25)
project(shop VERSION 1.0 LANGUAGES CXX)

# ---- library: আসল কাজ ----
add_library(shop_core STATIC
    src/inventory.cpp
    src/pricing.cpp)
add_library(shop::core ALIAS shop_core)
target_include_directories(shop_core PUBLIC include)
target_compile_features(shop_core PUBLIC cxx_std_23)

# ---- প্রোগ্রাম: পাতলা খোলস ----
add_executable(shop_cli apps/main.cpp)
target_link_libraries(shop_cli PRIVATE shop::core)

# ---- টেস্ট ----
enable_testing()
add_executable(shop_tests tests/pricing_test.cpp)
target_link_libraries(shop_tests PRIVATE shop::core)
add_test(NAME pricing COMMAND shop_tests)
#pragma once

namespace shop::pricing {
    inline constexpr long vat_percent = 15;

    long with_vat(long paisa);                  // ঘোষণা
    long apply_discount(long paisa, int pct);   // ঘোষণা
}
#include "shop/pricing.h"

namespace shop::pricing {
    long with_vat(long paisa) {
        return paisa + paisa * vat_percent / 100;
    }
    long apply_discount(long paisa, int pct) {
        return paisa - paisa * pct / 100;
    }
}
#include "shop/pricing.h"
#include <print>

int main() {
    long bill = shop::pricing::with_vat(65000);
    std::println("Bill: {}.{:02} Tk", bill / 100, bill % 100);
}

লক্ষ করুন CMakeLists.txt-এ কোথাও -I, -std= বা -l নেই। আছে শুধু কী বানাতে হবে আর কে কাকে ব্যবহার করে। এই প্রজেক্ট ৫০০ ফাইলে বড় হলে সাধারণত প্রতিটি ফোল্ডারে আলাদা একটি CMakeLists.txt রাখা হয়, আর মূল ফাইল সেগুলোকে add_subdirectory দিয়ে ডেকে আনে — add_subdirectory(src), add_subdirectory(apps). প্রতিটি দলের রেসিপি তার নিজের ফোল্ডারে থাকে।

পাতার রানার একবারে একটিই ফাইল চালাতে পারে, তাই নিচে library আর প্রোগ্রামের কাজটুকু এক ফাইলে রাখা হলো। বাস্তবে namespace shop::pricing অংশটি থাকত shop_core-এ, আর main থাকত shop_cli-তে:

#include <print>
#include <string>
#include <vector>

namespace shop::pricing {               // real project: shop_core library
    long with_vat(long paisa) { return paisa + paisa * 15 / 100; }
}

struct Item { std::string name; long paisa; int qty; };

int main() {                            // real project: shop_cli program
    std::vector<Item> cart{{"Rice", 8000, 5}, {"Tea", 25000, 1}};
    long total = 0;
    for (const auto& it : cart) total += it.paisa * it.qty;
    std::println("Items: {}", cart.size());
    std::println("Subtotal: {}.{:02} Tk", total / 100, total % 100);
    long bill = shop::pricing::with_vat(total);
    std::println("With VAT: {}.{:02} Tk", bill / 100, bill % 100);
}

চাল ৫ কেজি × ৮০ টাকা আর চা ১ প্যাকেট × ২৫০ টাকা — মোট ৬৫০ টাকা, ১৫% VAT যোগে ৭৪৭.৫০ টাকা। টাকা রাখা হয়েছে পয়সায় (long), যাতে দশমিকের floating-point গোলমাল না হয়। এখানে দেখার মূল বিষয় কোড নয়, বিভাজন: with_vat কোনো ইনপুট-আউটপুট করে না, তাই তাকে library-তে রেখে টেস্ট করা সহজ (অংশ ৯), আর main শুধু দেখায়। ভালো প্রজেক্ট-বিন্যাস আসলে ভালো ডিজাইনেরই প্রতিফলন।

৭ · Build type, warning, sanitizer আর presets

অংশ ১-এর চতুর্থ সমস্যা ছিল Debug আর Release. CMake-এ এর নাম build type, আর চারটি তৈরি আছে:

Build typeGCC/Clang-এ সাধারণ flagকখন
Debug-gলেখা আর debug করার সময়; optimization নেই, assert চালু
Release-O3 -DNDEBUGচূড়ান্ত প্রোগ্রাম; দ্রুত, assert বন্ধ
RelWithDebInfo-O2 -g -DNDEBUGদ্রুত, কিন্তু crash হলে কোন লাইনে তা দেখা যায় — বাস্তবে খুব কাজের
MinSizeRel-Os -DNDEBUGছোট ফাইল চাই, যেমন embedded

কোথায় বাছবেন তা generator-এর ওপর নির্ভর করে। Ninja আর Makefile হলো single-config generator — একটি build ফোল্ডারে একটিই ধরন, configure-এর সময় বলে দিতে হয়: cmake -S . -B build-rel -DCMAKE_BUILD_TYPE=Release. কিছু না বললে কোনো optimization বা debug flag ছাড়াই build হয়, যা প্রায়ই কারও কাম্য নয়। Visual Studio আর Xcode হলো multi-config — একই ফোল্ডারে সব ধরন থাকে, build-এর সময় বাছতে হয়: cmake --build build --config Release.

Release-এর -DNDEBUG লক্ষ করুন। NDEBUG সংজ্ঞায়িত থাকলে <cassert>-এর assert পুরো মুছে যায়। নিচের প্রোগ্রামটি নিজেই বলে দেয় সে কোন ধাঁচে build হয়েছে:

#include <cassert>
#include <print>

int stock_after_sale(int stock, int sold) {
    assert(sold <= stock && "cannot sell more than stock");
    return stock - sold;
}

int main() {
#ifdef NDEBUG
    std::println("Release-style build: NDEBUG defined, assert is OFF");
#else
    std::println("Debug-style build: NDEBUG not defined, assert is ON");
#endif
    std::println("__cplusplus = {}", __cplusplus);
    std::println("Stock left: {}", stock_after_sale(10, 3));
}

পাতার রানার কোনো -DNDEBUG দেয় না, তাই এখানে “Debug-style” আসে, আর __cplusplus দেখায় C++23-এর সংখ্যা (পর্ব ০৩-এর টেবিল মনে করুন)। একই ফাইল CMake-এর Release build-এ ছাপত “Release-style”, আর stock_after_sale(10, 30) ডাকলে assert আর থামাত না — ঋণাত্মক স্টক চুপচাপ বেরিয়ে যেত। শিক্ষা: assert শুধু debug-এর পাহারা। ব্যবহারকারীর ইনপুটের যাচাই কখনো assert-এ রাখবেন না; সেটা সাধারণ if বা exception-এর কাজ (পর্ব ৩৭)।

Warning — প্রতিটি target-এ, compiler বুঝে

পর্ব ০৩ থেকে আপনি -Wall -Wextra ছাড়া compile করেন না। CMake-এ এটি target-এর property, আর GCC/Clang আর MSVC-র flag আলাদা বলে একটি সরল শর্ত লাগে:

if(MSVC)
    target_compile_options(shop_core PRIVATE /W4 /permissive-)
else()
    target_compile_options(shop_core PRIVATE -Wall -Wextra -Wpedantic)
endif()

এখানে PRIVATE কেন? কারণ আপনার warning-এর পছন্দ আপনার নিজের কোডের জন্য; ব্যবহারকারীর ঘাড়ে চাপানো উচিত নয়। একই কাজ এক লাইনে করা যায় generator expression দিয়ে — $<...> আকারের ছোট শর্ত, যা configure-এর সময় নয়, build ফাইল লেখার সময় মূল্যায়ন হয়। যেমন "$<$<CONFIG:Debug>:-Og>" মানে “শুধু Debug build-এ -Og.” পড়তে কঠিন বলে শুরুতে if() ভালো; অন্যদের CMake ফাইলে এগুলো দেখলে অন্তত চিনতে পারবেন।

Sanitizer — একটি চালু-বন্ধের সুইচ

পর্ব ৪৩-এ দেখেছেন, টেস্ট চালানোর সময় AddressSanitizer আর UBSan কত ভুল ধরে। CMake-এ এগুলোকে একটি option-এর পেছনে রাখুন, যাতে দরকারমতো চালু করা যায়:

option(SHOP_SANITIZE "Build with ASan and UBSan" OFF)
if(SHOP_SANITIZE AND NOT MSVC)
    target_compile_options(shop_core PUBLIC -fsanitize=address,undefined
                                            -fno-omit-frame-pointer)
    target_link_options(shop_core PUBLIC -fsanitize=address,undefined)
endif()

এবার PUBLIC — কারণ sanitizer-এর runtime প্রোগ্রাম আর টেস্ট দুটোর link-এও লাগে, আর তাদের নিজের কোডও যাচাই হওয়া উচিত। চালু করতে: cmake -S . -B build-asan -DSHOP_SANITIZE=ON.

CMakePresets.json — লম্বা কমান্ডের বদলে একটি নাম

এতক্ষণে configure-কমান্ড লম্বা হয়ে গেছে: generator, build type, sanitizer… দলের সবাই যাতে একই কমান্ড ব্যবহার করেন, CMake ৩.১৯ থেকে CMakePresets.json ফাইল আছে। এটি Git-এ থাকে, আর IDE-গুলোও পড়ে:

{
  "version": 6,
  "configurePresets": [
    { "name": "debug", "generator": "Ninja",
      "binaryDir": "build/debug",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug",
                          "SHOP_SANITIZE": "ON",
                          "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } },
    { "name": "release", "generator": "Ninja",
      "binaryDir": "build/release",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } }
  ]
}
cmake --preset debug          # configure, সব সেটিং ফাইল থেকে
cmake --build build/debug     # build

ব্যক্তিগত সেটিং (নিজের compiler-এর ঠিকানা ইত্যাদি) রাখার জন্য আছে CMakeUserPresets.json, যা Git-এ যায় না। “version” সংখ্যাটি ফাইলের ধরনের version — নতুন সংখ্যা মানে নতুন CMake লাগবে; আপনার দলের সবার CMake যেটা বোঝে, সেটাই দিন।

compile_commands.json — IDE আর টুলের জন্য মানচিত্র

উপরের preset-এ CMAKE_EXPORT_COMPILE_COMMANDS চালু আছে। এতে CMake build ফোল্ডারে compile_commands.json নামের একটি ফাইল লেখে: প্রতিটি .cpp-এর জন্য ঠিক কোন কমান্ড, কোন include ফোল্ডার, কোন flag. VS Code-এর clangd, clang-tidy আর অনেক টুল এই ফাইল পড়েই আপনার কোড বোঝে — এটি ছাড়া এডিটরে “shop/pricing.h not found”-এর লাল দাগ দেখা যায়, যদিও build ঠিকই চলে। এটি Ninja আর Makefile generator-এ কাজ করে; Visual Studio generator-এ নয়।

৮ · বাইরের library — find_package, FetchContent আর package manager

বাস্তব প্রজেক্ট সব কিছু নিজে লেখে না। টাকা সুন্দর করে ছাপাতে fmt, JSON পড়তে একটি JSON library, টেস্টের জন্য GoogleTest. প্রশ্ন হলো, এগুলো আপনার মেশিনে আসবে কীভাবে, আর CMake কীভাবে জানবে কোথায় আছে। তিনটি পথ:

পথ ১ — find_package: আগে থেকে বসানো library খোঁজা

find_package(fmt CONFIG REQUIRED)      # না পেলে configure থামবে
find_package(Threads REQUIRED)          # পর্ব ৪৫-এর thread-এর জন্য
target_link_libraries(shop_core PRIVATE fmt::fmt Threads::Threads)

find_package মেশিনে বসানো library খোঁজে — Linux-এ apt install libfmt-dev দিয়ে বসানো, বা কোনো package manager-এর ফোল্ডারে। পেলে সে একটি imported target দেয়, যেমন fmt::fmt, যার ভেতরে include ফোল্ডার, library ফাইল আর দরকারি flag সব আগে থেকে ভরা। আপনি শুধু link করেন। পুরোনো ধাঁচে মানুষ ${FMT_INCLUDE_DIRS} আর ${FMT_LIBRARIES}-এর মতো variable হাতে বসাত; imported target সেই ঝামেলা দূর করেছে।

পথ ২ — FetchContent: build-এর সময় নামিয়ে আনা

মেশিনে বসানো না থাকলে? CMake নিজেই source নামিয়ে আপনার প্রজেক্টের অংশ হিসেবে build করতে পারে — FetchContent দিয়ে। টেস্টের জন্য GoogleTest-এর উদাহরণ:

include(FetchContent)
FetchContent_Declare(googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG        v1.15.2)            # নির্দিষ্ট version — কখনো "main" নয়
FetchContent_MakeAvailable(googletest)

add_executable(shop_tests tests/pricing_test.cpp)
target_link_libraries(shop_tests PRIVATE shop::core GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(shop_tests)
  1. DeclareGIT_TAG v1.15.2কোথা থেকে, কোন version
  2. নামানোbuild/_deps/configure-এর সময়
  3. TargetGTest::gtest_mainযেন নিজের library
  4. Link + testctestটেস্ট চলে
FetchContent_Declareশুধু ঘোষণা
# এখনো কিছু নামেনি — শুধু CMake মনে রাখল:
# "googletest" নামের জিনিস এই ঠিকানায়, এই tag-এ
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG        v1.15.2
FetchContent_MakeAvailableপ্রথম configure-এ
build/_deps/
├── googletest-src/     # নামানো source
├── googletest-build/   # তার নিজের build
└── googletest-subbuild/
# পরের configure-এ আবার নামায় না
ভেতরে যা ঘটেadd_subdirectory-এর মতো
# googletest-src/CMakeLists.txt আপনার প্রজেক্টের অংশ হয়ে চলে
নতুন target:  GTest::gtest  GTest::gtest_main
              GTest::gmock  GTest::gmock_main
# প্রতিটির include, flag — usage requirements-সহ
$ ctest --test-dir build --output-on-failureফল
    Start 1: Pricing.AddsVat
1/2 Test #1: Pricing.AddsVat ..........   Passed
    Start 2: Pricing.Discount
2/2 Test #2: Pricing.Discount .........   Passed
100% tests passed, 0 tests failed out of 2
  1. ঘোষণা। FetchContent_Declare শুধু লিখে রাখে কোন library, কোথা থেকে, কোন version. নির্দিষ্ট tag (বা আরও নিরাপদে commit hash) দেওয়া জরুরি — “main” দিলে আজ এক কোড, কাল আরেক, আর আপনার build একদিন বিনা কারণে ভেঙে যাবে।
  2. নামানো। FetchContent_MakeAvailable configure-এর সময় source নামিয়ে build/_deps/-এ রাখে। এর জন্য ইন্টারনেট আর Git লাগে প্রথমবার; পরে আর নামায় না। build/ মুছলে আবার নামাবে।
  3. Target হয়ে যাওয়া। নামানো প্রজেক্টের নিজের CMakeLists.txt আপনার প্রজেক্টের ভেতরে চলে, যেন আপনিই add_subdirectory করেছেন। ফলে তার সব target আপনার হাতে, আপনার compiler আর flag দিয়ে build হয় — ABI মিল নিয়ে চিন্তা নেই। (Windows-এ MSVC-তে GoogleTest-এর ডকুমেন্টেশন একটি বাড়তি লাইন gtest_force_shared_crt চালু করার পরামর্শ দেয়।)
  4. Link আর টেস্ট। GTest::gtest_main link করলে নিজের main লিখতে হয় না। gtest_discover_tests টেস্ট-প্রোগ্রামের ভেতরের প্রতিটি টেস্টকে আলাদা করে ctest-কে জানায়, তাই রিপোর্টে প্রতিটির নাম আলাদা আসে। টেস্ট লেখার পুরো কৌশল পর্ব ৪৮-এ।

FetchContent সুবিধাজনক, কিন্তু খরচও আছে: প্রতিটি নতুন build ফোল্ডারে আবার নামানো আর পুরো library compile করা, আর অফিসের ইন্টারনেট বন্ধ থাকলে configure-ই হয় না। ছোট বা টেস্টের library-র জন্য এটা দারুণ; বড়, ভারী library-র জন্য পরের পথটি ভালো।

পথ ৩ — package manager: vcpkg আর Conan

C++-এর কোনো “অফিসিয়াল” package manager নেই (কেন নেই — ভাবনার প্রশ্ন ০৩)। সবচেয়ে প্রচলিত দুটি: Microsoft-এর vcpkg আর JFrog-সমর্থিত Conan. দুটোই একই কাজ করে: library-গুলো আপনার compiler আর প্ল্যাটফর্মের জন্য build করে (বা আগে build করা কপি নামিয়ে) একটি ফোল্ডারে রাখে, আর CMake-কে জানায় সেখানে খুঁজতে। ফলে আপনার CMakeLists.txt-এ থাকে শুধু সাধারণ find_package(fmt CONFIG REQUIRED) — library কোথা থেকে এল, রেসিপি তা জানেই না। vcpkg-তে প্রজেক্টের মূল ফোল্ডারে একটি vcpkg.json ফাইলে তালিকা থাকে:

{
  "name": "shop",
  "dependencies": [ "fmt", "gtest" ]
}

তারপর configure-এর সময় vcpkg-এর একটি toolchain file দিয়ে দিতে হয় (সাধারণত preset-এর toolchainFile-এ, বা -DCMAKE_TOOLCHAIN_FILE=... দিয়ে), যা vcpkg/scripts/buildsystems/vcpkg.cmake-এ থাকে। বাকি সব আগের মতোই।

পথসুবিধাঅসুবিধা
find_package (system)দ্রুত, কিছু নামাতে হয় নাপ্রতিটি মেশিনে আলাদা version থাকতে পারে
FetchContentশুধু CMake লাগে, version রেসিপিতেইপ্রতিটি build ফোল্ডারে নামানো ও compile
vcpkg / Conanহাজারো library, version-লক, একবার build করে বারবার ব্যবহারআরেকটি টুল শেখা ও বসানো

৯ · টেস্টের এক ঝলক আর সাধারণ ভুলগুলো

অংশ ৬-এর CMakeLists.txt-এ তিনটি লাইন দেখেছেন: enable_testing(), একটি টেস্ট-প্রোগ্রাম, আর add_test. এর মানে, CMake আপনার প্রজেক্টের টেস্টের একটি তালিকা রাখে, আর ctest নামের সহকারী টুল সেই তালিকা চালায়। কোনো টেস্ট-প্রোগ্রাম ০ ফেরত দিলে পাস, অন্য কিছু দিলে ফেল — খুব সরল চুক্তি।

cmake --build build
ctest --test-dir build --output-on-failure    # ফেল হলে প্রোগ্রামের আউটপুট দেখাও
ctest --test-dir build -R pricing              # নামে "pricing" আছে শুধু সেগুলো

বড় প্রজেক্টে প্রতিটি পরিবর্তনের পর এই দুই কমান্ড একটি সার্ভারে নিজে নিজে চলে — এর নাম continuous integration. Unit test কীভাবে লিখবেন, GoogleTest-এর EXPECT_EQ, debugger আর CI — সব পর্ব ৪৮-এ। এখানে শুধু মনে রাখুন: টেস্ট হলো আরেকটি target, যা আপনার library-কে link করে — ঠিক যেমন shop_cli. এজন্যই অংশ ৬-এ কাজ library-তে রাখা হয়েছিল।

যে ভুলগুলো সবচেয়ে বেশি দেখা যায়

ভুলকেন সমস্যাঠিক উপায়
include_directories(include)ফোল্ডারের সব target পায়, কে কার ওপর নির্ভর করে তা হারিয়ে যায়target_include_directories + PUBLIC/PRIVATE
link_libraries(fmt)সব target-এ link, ব্যবহারকারীর কাছে কী যাবে বোঝা যায় নাtarget_link_libraries(t PRIVATE fmt::fmt)
set(CMAKE_CXX_FLAGS "-std=c++17 -Wall")compiler-নির্দিষ্ট flag সবার ঘাড়ে, MSVC-তে ভাঙে, standard দুবার বসেtarget_compile_features, target_compile_options
file(GLOB SRC src/*.cpp)নতুন .cpp যোগ করলে CMake আবার না চালানো পর্যন্ত সে বাদ থাকেফাইলের নাম হাতে তালিকায় লিখুন
প্রজেক্ট ফোল্ডারে cmake .in-source build — source-এ আবর্জনা, পরিষ্কার করা কঠিনcmake -S . -B build
cmake_minimum_required(VERSION 2.8)পুরোনো policy চালু হয়; CMake ৪.০ থেকে ৩.৫-এর নিচের সংখ্যা errorআপনি যে version-এ পরীক্ষা করেছেন তার কাছাকাছি, অন্তত ৩.২০+
Release build-এ CMAKE_BUILD_TYPE না দেওয়া (Ninja/Make)optimization ছাড়া “release” — ধীর প্রোগ্রাম-DCMAKE_BUILD_TYPE=Release বা preset

file(GLOB) নিয়ে একটু বেশি বলা দরকার, কারণ এটা দেখতে খুব সুবিধাজনক। এটি configure-এর মুহূর্তে ফোল্ডারে যা আছে তার একটি তালিকা বানায়। পরে আপনি নতুন report.cpp যোগ করলেন — CMakeLists.txt বদলায়নি, তাই Ninja CMake-কে আবার চালায় না, নতুন ফাইলটি build-এ ঢোকে না, আর আপনি পান রহস্যময় linker error. CMake-এর নিজের ডকুমেন্টেশনই source জোগাড়ে GLOB ব্যবহার করতে নিরুৎসাহিত করে। CONFIGURE_DEPENDS নামের একটি বিকল্প আছে যা প্রতি build-এ ফোল্ডার আবার দেখে, তবে সেটা সব generator-এ নির্ভরযোগ্য নয় আর build ধীর করে। হাতে লেখা তালিকা একটু খাটুনির, কিন্তু Git-এর ইতিহাসে স্পষ্ট দেখা যায় কোন ফাইল কবে যোগ হলো।

পরীক্ষা ও ইন্টারভিউতে

প্র: CMake কি compiler? উ: না; এটি build system generator — Ninja, Make বা Visual Studio-র build ফাইল লেখে, আর সেই টুল compiler চালায়।

প্র: target_link_libraries-এ PUBLIC আর PRIVATE-এর পার্থক্য কী? উ: PRIVATE শুধু ওই target-এর জন্য; PUBLIC ওই target আর তার সব ব্যবহারকারীর জন্য। INTERFACE শুধু ব্যবহারকারীর জন্য।

প্র: static আর shared library-র পার্থক্য? উ: static (.a/.lib) link-এর সময় প্রোগ্রামে কপি হয়; shared (.so/.dll) চলার সময় আলাদা ফাইল থেকে লোড হয়, কয়েকটি প্রোগ্রাম ভাগ করে নেয়।

প্র: out-of-source build কেন? উ: তৈরি ফাইল source থেকে আলাদা থাকে; build ফোল্ডার মুছে নিরাপদে আবার শুরু করা যায়, আর পাশাপাশি Debug-Release রাখা যায়।

প্র: একটি .cpp বদলালে কী আবার build হয়? উ: শুধু তার object file, তারপর যে library ও প্রোগ্রাম সেটি ধারণ করে তাদের link; একটি header বদলালে যারা সেটি include করে সবাই।

১০ · AI-এর যুগে — AI-এর লেখা CMake যাচাই

AI-কে “আমার প্রজেক্টের CMakeLists.txt লিখে দাও” বললে সে কয়েক সেকেন্ডে একটি ফাইল দেয়, আর প্রায়ই সেটা চলেও। সমস্যা অন্য জায়গায়: ইন্টারনেটে CMake-এর লেখার বড় অংশ ২০১০-এর দশকের শুরুর, পুরোনো ধাঁচের। AI সেখান থেকেই শিখেছে। তাই তার CMake প্রায়ই “আজ চলে, কাল বড় হলে ভাঙে” ধরনের। যে ভুলগুলো বারবার দেখা যায়:

AI যা লেখেসমস্যাযা হওয়া উচিত
cmake_minimum_required(VERSION 3.0) বা 2.8পুরোনো policy; নতুন CMake-এ warning বা errorবাস্তব version, ৩.২০+
include_directories, link_librariesglobal, নির্ভরতা অস্পষ্টtarget_* কমান্ড
CMAKE_CXX_FLAGS-এ -std=, -Wall, -O2MSVC-তে ভাঙে, build type-এর সাথে লড়াইtarget_compile_features, target_compile_options
সব target_link_libraries PUBLIC, বা কোনো শব্দই নেইঅদরকারি নির্ভরতা ছড়ায়, পুরোনো ধাঁচের সাথে মেশেপ্রতিটি নির্ভরতায় ভেবে PRIVATE/PUBLIC
file(GLOB_RECURSE ...)নতুন ফাইল বাদ পড়েস্পষ্ট তালিকা
FetchContent-এ GIT_TAG mainঅনির্দিষ্ট version, build হঠাৎ ভাঙেনির্দিষ্ট tag বা commit hash
নেই এমন target-নাম, যেমন ভুল বানানে GTest::Mainconfigure-এ error, বা ভুল linklibrary-র ডকুমেন্টেশনে আসল নাম দেখা

AI-কে শুধু “লিখে দাও” না বলে, আপনার প্রজেক্টের তথ্য আর নিয়ম স্পষ্ট করে দিন — তাহলে ফল অনেক ভালো আসে। নিচের prompt-টি কপি করে নিজের তথ্য বসান:

তুমি একজন অভিজ্ঞ C++ build engineer. আমার প্রজেক্টের জন্য modern CMake (target-ভিত্তিক) লিখে দাও। প্রজেক্ট: - library: shop_core (src/inventory.cpp, src/pricing.cpp), public header include/shop/ - প্রোগ্রাম: shop_cli (apps/main.cpp) - টেস্ট: shop_tests (tests/pricing_test.cpp), GoogleTest দিয়ে - C++23, Windows-এ MSVC আর Linux-এ GCC দুটোতেই চলবে নিয়ম: ১. cmake_minimum_required 3.25; include_directories, link_libraries, CMAKE_CXX_FLAGS ব্যবহার করবে না ২. প্রতিটি target_link_libraries-এ PUBLIC/PRIVATE দাও, আর প্রতিটির পাশে এক লাইনে কারণ লেখো ৩. warning: MSVC-তে /W4, অন্যগুলোতে -Wall -Wextra -Wpedantic, শুধু আমার target-এ ৪. file(GLOB) নয়; ফাইলের নাম তালিকায় ৫. GoogleTest FetchContent দিয়ে, নির্দিষ্ট version tag ৬. debug আর release নামে দুটি CMakePresets.json preset শেষে বলো কোন লাইন কোন CMake version-এ এসেছে, যেখানে নিশ্চিত নও সেখানে "নিশ্চিত নই" লেখো।

AI-এর লেখা CMake যাচাইয়ের চেকলিস্ট

শেষ কথা: CMake ফাইলও কোড। এটি Git-এ থাকে, রিভিউ হয়, আর এর বাগ প্রজেক্টের সবাইকে ভোগায়। AI-এর দেওয়া CMake প্রথম খসড়া হিসেবে দারুণ — তার প্রতিটি target_link_libraries-এর শব্দটি যে আপনি নিজে বুঝে রেখেছেন, সেটাই আপনাকে আলাদা করবে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১১৯৭৬ সালের make-এর মূল ধারণা প্রায় ৫০ বছর পরেও Ninja আর CMake-এর ভেতরে টিকে আছে। কোন ধারণা, আর কেন এত টেকসই?

make লিখেছিলেন Stuart Feldman, ১৯৭৬ সালে Bell Labs-এ — যে প্রতিষ্ঠানে Unix আর C জন্মেছে। প্রচলিত গল্প হলো, এক সহকর্মী একটি প্রোগ্রাম debug করতে গিয়ে সারা সকাল নষ্ট করেছিলেন, কারণ তিনি একটি বদলানো ফাইল আবার compile করতে ভুলে গিয়েছিলেন, আর পুরোনো object file দিয়েই পরীক্ষা চালাচ্ছিলেন। Feldman-এর সমাধান ছিল একটি সরল কিন্তু গভীর ধারণা: প্রতিটি তৈরি ফাইলের সাথে তার উৎস আর তৈরির কমান্ড লিখে রাখো, আর timestamp তুলনা করে শুধু পুরোনো হয়ে যাওয়া অংশ আবার বানাও। এই কাজের জন্য তিনি ২০০৩ সালে ACM Software System Award পান।

ধারণাটির টেকসই হওয়ার কারণ, এটি আসলে একটি গণিতের কাঠামো — directed acyclic graph (চক্রহীন নির্দেশিত graph). প্রতিটি ফাইল একটি বিন্দু, প্রতিটি নির্ভরতা একটি তীর। “কী আবার বানাতে হবে” মানে graph-এ বদলানো বিন্দু থেকে নিচের দিকে হাঁটা; “কোনটা একসাথে চালানো যায়” মানে যাদের মধ্যে কোনো পথ নেই। এই প্রশ্নগুলোর উত্তর ভাষা, compiler বা যুগের ওপর নির্ভর করে না। আজকের Ninja, Bazel, এমনকি JavaScript আর Rust-এর build টুলও একই graph-এর ওপর দাঁড়িয়ে; পার্থক্য শুধু “বদলেছে কি না” কীভাবে মাপা হয় (timestamp, নাকি ফাইলের ভেতরের hash) আর graph কে লেখে।

make-এর দুর্বলতাগুলোও শিক্ষণীয়। বিখ্যাত উদাহরণ: Makefile-এ কমান্ডের লাইন অবশ্যই একটি Tab অক্ষর দিয়ে শুরু হতে হয়, স্পেস দিলে চলে না। Feldman পরে বলেছেন, তিনি দ্রুতই বুঝেছিলেন এটা ভুল নকশা, কিন্তু ততদিনে মাত্র ডজনখানেক ব্যবহারকারী থাকলেও তিনি তাদের ফাইল ভাঙতে চাননি — আর সেই সিদ্ধান্ত পরের কয়েক দশকের লক্ষ লক্ষ ব্যবহারকারী বহন করছে। আরেকটি সমস্যা ১৯৯০-এর দশকের শেষে Peter Miller-এর “Recursive Make Considered Harmful” লেখায় বিখ্যাত হয়: প্রতিটি ফোল্ডারে আলাদা Makefile আর একটি আরেকটিকে ডাকলে কোনো Makefile-ই পুরো graph দেখে না, ফলে নির্ভরতা হারায় আর build ভুল হয়।

CMake-এর add_subdirectory ঠিক এই শিক্ষা মেনে তৈরি: অনেক ফাইলে রেসিপি ভাগ করা যায়, কিন্তু CMake সবগুলো পড়ে একটি সম্পূর্ণ graph বানায়, আর Ninja সেই পুরো graph একসাথে দেখে।

মূল উপলব্ধি: build system-এর প্রাণ হলো একটি সঠিক dependency graph; টুল বদলায়, ধারণা বদলায় না। আর ছোট নকশা-ভুল, যেমন Tab-এর নিয়ম, একবার ব্যবহারকারী পেয়ে গেলে দশকের পর দশক টিকে থাকে — আপনার নিজের API-র ক্ষেত্রেও এটা সত্য।

প্র ০২CMake নিজে build না করে কেন অন্য টুলের জন্য ফাইল লেখে? এই “generator” নকশার লাভ আর দাম কী?

১৯৯৯-২০০০ সালের দিকে Kitware যখন ITK-র জন্য CMake বানাচ্ছিল, তখন দুই জগৎ পুরো আলাদা ছিল। Unix-এ সবাই make চালাত, প্রায়ই Autotools (./configure-এর সেই বিখ্যাত script) দিয়ে Makefile তৈরি করে। Windows-এ ডেভেলপাররা Visual Studio-র ভেতরেই কাজ করতেন, আর সেখানে প্রজেক্ট মানে Visual Studio-র নিজস্ব ফাইল। একটি নতুন build টুল বানিয়ে দুই জগৎকে বলা “আপনাদের অভ্যাস ছেড়ে এটা ব্যবহার করুন” — এটা প্রায় নিশ্চিতভাবে ব্যর্থ হতো। তাই CMake বেছে নিল চতুর পথ: “আপনি আপনার প্রিয় টুলই ব্যবহার করুন; আমি শুধু সেই টুলের ভাষায় ফাইল লিখে দেব।” Windows-ব্যবহারকারী পেলেন আসল Visual Studio প্রজেক্ট, Unix-ব্যবহারকারী পেলেন আসল Makefile. এর আগে VTK প্রজেক্টে Kitware-এর লোকেরা একই ধারণার ছোট সংস্করণ ব্যবহার করেছিলেন, CMake তার পরিণত রূপ।

লাভ শুধু সেই সময়ের জন্য নয়। ২০১২ সালের দিকে CMake-এ Ninja generator যোগ হলো — আর রাতারাতি হাজারো CMake প্রজেক্ট কোনো রেসিপি না বদলে দ্রুততর build পেল। IDE-গুলোও একই সুবিধা পেল: CLion বা VS Code-কে নতুন build-ভাষা শিখতে হয়নি, তারা CMake-কে জিজ্ঞেস করে target-এর তালিকা পায় (CMake এজন্য একটি “file API” দেয়)। অর্থাৎ বর্ণনা আর বাস্তবায়ন আলাদা রাখায় দুই দিক স্বাধীনভাবে উন্নত হতে পেরেছে — সফটওয়্যার ডিজাইনের একটি পুরোনো নীতি।

দাম কী? প্রথমত, দুই স্তর মানে দুই জায়গায় ভুল: কোনো সমস্যা CMake-এর configure-এ, নাকি Ninja-র build-এ — নতুনদের কাছে বোঝা কঠিন। দ্বিতীয়ত, CMake-এর নিজের ভাষা। এটি ধাপে ধাপে বেড়েছে, পরিকল্পনা করে নকশা করা হয়নি; সবকিছু string, list মানে সেমিকোলন দিয়ে জোড়া string, আর পুরোনো আর নতুন ধাঁচ একই ভাষায় পাশাপাশি টিকে আছে। এজন্যই অংশ ৫-এর “modern CMake” আলাদা নাম পেয়েছে: CMake ৩.০ (২০১৪)-র আশেপাশে target-ভিত্তিক usage requirements পরিণত হয়, আর ২০১৭ সালের দিকে Daniel Pfeifer-এর “Effective CMake”-এর মতো বক্তৃতা এই ধাঁচকে জনপ্রিয় করে। কিন্তু পুরোনো টিউটোরিয়াল ইন্টারনেট থেকে মুছে যায়নি — আর অংশ ১০-এ দেখেছেন, AI-ও সেগুলো থেকে শিখেছে।

মূল উপলব্ধি: CMake জিতেছে সবচেয়ে সুন্দর বলে নয়, সবার অভ্যাসকে সম্মান করে মাঝখানে বসেছে বলে। বর্ণনা (কী বানাতে হবে) আর বাস্তবায়ন (কীভাবে বানাতে হবে) আলাদা রাখলে প্রতিটি দিক নিজে নিজে উন্নত হতে পারে।

প্র ০৩Python-এ pip, JavaScript-এ npm, Rust-এ cargo — C++-এ এমন একটি standard package manager নেই কেন?

প্রথম কারণ ইতিহাস। C++ standard হয়েছে ১৯৯৮ সালে, কিন্তু ভাষাটি তার আগেই দেড় দশক ধরে ব্যবহার হচ্ছিল — হাজারো প্রতিষ্ঠানে, হাজারো নিজস্ব build ব্যবস্থায়। ISO-র C++ standard শুরু থেকেই শুধু ভাষা আর library বর্ণনা করে; কোন ফাইল কোথায় থাকবে, কীভাবে compile হবে, এমনকি “ফাইল” শব্দটি পর্যন্ত standard-এর মূল ধারণায় নেই। Rust উল্টো পথে এসেছে: ২০১৫ সালে Rust ১.০-এর সাথেই cargo ছিল, তাই প্রথম দিন থেকে প্রতিটি Rust প্রজেক্ট একই ধাঁচে। C++-এর সেই “প্রথম দিন” অনেক আগে চলে গেছে।

দ্বিতীয় কারণ প্রযুক্তিগত, আর আরও কঠিন: C++-এর library বিতরণ করা কঠিন। Python বা JavaScript-এ library সাধারণত source আকারে যায়, আর একই interpreter সবখানে চলে। C++ library compile হয় machine code-এ, আর সেই কোড নির্ভর করে compiler (GCC, Clang, MSVC), তার version, standard library (libstdc++ না libc++), Debug না Release, এমনকি কিছু flag-এর ওপর। এদের মধ্যে ABI না মিললে link হয় না, বা হয়েও চলার সময় ভাঙে। Windows-এ MSVC-র Debug আর Release runtime পর্যন্ত আলাদা। ফলে “fmt ১১” একটি package নয়, শত রকমের সম্ভাব্য binary. এজন্যই vcpkg মূলত source থেকে আপনার মেশিনে build করে, আর Conan প্রতিটি package-এর জন্য compiler, version, build type ইত্যাদির হিসাব রেখে আলাদা binary রাখে।

তৃতীয় কারণ build system-এর ভাগাভাগি। একটি package manager-কে প্রতিটি library build করতে জানতে হয় — কিন্তু কেউ CMake ব্যবহার করে, কেউ Meson, কেউ Autotools, কেউ Bazel, কেউ হাতে লেখা Makefile. vcpkg-এর হাজারো “port” আসলে প্রতিটি library-কে কীভাবে build করতে হয় তার আলাদা রেসিপি। WG21-এর ভেতরে SG15 নামের একটি study group বহু বছর ধরে tooling নিয়ে আলোচনা করছে — যেমন modules-এর জন্য build system আর compiler কীভাবে তথ্য বিনিময় করবে, বা একটি library নিজের বর্ণনা কোন সাধারণ ফরম্যাটে দেবে। কিন্তু standard-এ একটি package manager ঘোষণা করে দেওয়া হলেও বিদ্যমান কোটি কোটি লাইন কোড রাতারাতি সেটায় আসবে না।

বাস্তব ফল: আজ বেশিরভাগ নতুন প্রজেক্ট CMake + vcpkg বা CMake + Conan বেছে নেয়, অনেক বড় প্রতিষ্ঠান সব কোড এক repository-তে রেখে নিজস্ব build ব্যবস্থা (যেমন Bazel) চালায়, আর ছোট প্রজেক্ট FetchContent-এ কাজ সারে। এটি বিশৃঙ্খল, কিন্তু কাজ করে।

মূল উপলব্ধি: C++-এ package manager-এর অভাব অলসতা নয়, ভাষার জন্ম-ইতিহাস আর native code-এর ABI-জটিলতার ফল। তাই একটি প্রজেক্ট শুরুর সময়েই ঠিক করে ফেলুন বাইরের library কোন পথে আসবে — পরে বদলানো অনেক কষ্টের।

প্র ০৪Ninja-র নিজের ভাষা প্রায় কিছুই পারে না, তবুও বড় প্রজেক্টে সে Make-এর চেয়ে লক্ষণীয়ভাবে দ্রুত। কীভাবে?

Ninja লিখেছিলেন Evan Martin, ২০১০-২০১১ সালের দিকে, Google Chrome ব্রাউজারের Linux build-এর জন্য। Chrome-এ তখন হাজার হাজার source ফাইল। সমস্যাটা ছিল অদ্ভুত: একটি ফাইলও না বদলে build চালালে — অর্থাৎ শুধু “কিছু করার আছে কি না” দেখতে — Make অনেক সেকেন্ড সময় নিত। ডেভেলপার দিনে শতবার build করেন, তাই প্রতিবারের এই অপেক্ষা যোগ হয়ে বিশাল অপচয়। Martin-এর লক্ষ্য ছিল এই “no-op build” প্রায় তাৎক্ষণিক করা।

তাঁর মূল সিদ্ধান্ত: Ninja-র ফাইল মানুষ লিখবে না, অন্য প্রোগ্রাম লিখবে। নিজের ওয়েবসাইটে তিনি Ninja-কে বলেছেন একটি build system-এর “assembler” — উচ্চস্তরের সিদ্ধান্ত (কোন flag, কোন প্ল্যাটফর্ম, কোন ফাইল) নেবে CMake বা Chrome-এর GN-এর মতো generator; Ninja শুধু তৈরি করা তালিকা যত দ্রুত সম্ভব চালাবে। ফলে Ninja-র ভাষায় শর্ত নেই, function নেই, wildcard নেই — Make-এ যেগুলো প্রতিবার পার্স আর মূল্যায়ন করতে সময় যায়। build.ninja পড়া প্রায় শুধু একটি সরল তালিকা পড়া।

দ্বিতীয় কৌশল header-নির্ভরতা। অংশ ৪-এ দেখেছেন, compiler প্রতিটি .cpp-এর জন্য একটি ছোট ফাইলে লেখে কোন কোন header পড়েছে। Make-এ এই হাজারো ছোট ফাইল প্রতিবার আবার পড়তে হয়। Ninja এগুলো একবার পড়ে নিজের একটি সংক্ষিপ্ত binary log-এ (.ninja_deps) জমা রাখে, আর আগের build-এর কমান্ডও মনে রাখে (.ninja_log), যাতে flag বদলালে ঠিক টের পায়। তৃতীয়ত, Ninja ডিফল্টভাবেই CPU-র সব core ব্যবহার করে কাজ ভাগ করে, যেখানে Make-এ -j আলাদা করে দিতে হয়। আর তার আউটপুট পরিষ্কার: একটি লাইনে অগ্রগতি, error হলে শুধু সেই কমান্ড।

লক্ষ করুন, compile নিজে Ninja দ্রুত করে না — g++ একই গতিতেই চলে। পুরো build-এ (সব ফাইল নতুন করে) Ninja আর Make-এর পার্থক্য তাই ছোট। পার্থক্য বিশাল হয় বড় প্রজেক্টের incremental আর no-op build-এ, যা ডেভেলপার সারাদিন করেন। পরে Chrome নিজের নতুন generator GN বানায়, যার নামই “Generate Ninja”-এর সংক্ষেপ, আর CMake, Meson-সহ অনেক টুল Ninja-কে প্রধান backend করেছে।

মূল উপলব্ধি: কখনো কখনো দ্রুত হওয়ার উপায় হলো কম করা। কাজ দুই স্তরে ভাগ করুন — বুদ্ধির কাজ একবার (generator), যান্ত্রিক কাজ বারবার আর খুব দ্রুত (Ninja). আর মাপুন ঠিক যে কাজটি মানুষ সবচেয়ে বেশিবার করে।

প্র ০৫Header-only library ব্যবহার করা সহজ, তবুও সব library header-only হয় না কেন? আর C++20 modules build-এর এই হিসাব কীভাবে বদলায়?

Header-only library-র আকর্ষণ পরিষ্কার: কোনো আলাদা build নেই, কোনো .a বা .dll নেই, ABI-র চিন্তা নেই। ফাইলগুলো প্রজেক্টে কপি করুন, #include দিন, কাজ শেষ। CMake-এ শুধু একটি INTERFACE target. এজন্যই nlohmann/json বা Boost-এর অনেক অংশ এভাবে আসে, আর template-ভিত্তিক library-র জন্য অনেক সময় এটাই একমাত্র উপায় — template-এর পুরো কোড ব্যবহারের জায়গায় দেখতে হয় (পর্ব ২৮–২৯)।

দাম দিতে হয় compile সময়ে। পর্ব ১১-এ দেখেছেন, #include মানে আক্ষরিক কপি-পেস্ট। একটি বড় header-only library যদি ২০০টি .cpp-তে include হয়, তার হাজার হাজার লাইন ২০০ বার parse হয়, আর template-গুলো প্রতিটি ফাইলে আলাদা করে তৈরি হয় — যার বেশিরভাগ পরে linker ফেলে দেয়। Compiled library-তে এই কাজ একবারই হয়, তার .cpp-তে। এছাড়া header-only library-র ভেতরের যেকোনো বদলে সব ব্যবহারকারী ফাইল আবার compile হয় (অংশ ৪-এর graph মনে করুন)। মাঝামাঝি পথও আছে: fmt দুইভাবেই ব্যবহার করা যায়, আর Catch2 টেস্ট-library তার version ৩-এ header-only থেকে compiled library-তে চলে গেছে — compile সময়ের অভিযোগই ছিল বড় কারণ।

C++20 modules এই সমীকরণ বদলাতে এসেছে (পর্ব ৪২)। একটি module একবার compile হয়ে একটি BMI (binary module interface) ফাইল বানায়; যারা import করে তারা সেই আগে-হজম-করা ফাইল পড়ে, শত শত লাইন আবার parse করে না। ফলে “ব্যবহারে সহজ” আর “compile-এ দ্রুত” একসাথে পাওয়ার সম্ভাবনা তৈরি হয়। কিন্তু build system-এর কাজ জটিল হয়: header-এর যুগে প্রতিটি .cpp যেকোনো ক্রমে compile হতে পারত; module-এ যে ফাইল import shop.pricing; লেখে, তার আগে shop.pricing module compile হতেই হবে। Build টুলকে তাই compile-এর আগে প্রতিটি ফাইল “scan” করে বের করতে হয় কে কাকে import করে — অর্থাৎ graph-টা আর CMake একা আগে থেকে জানে না, build-এর সময় আবিষ্কার করতে হয়।

এই কাজ compiler, CMake আর Ninja-র যৌথ প্রচেষ্টায় তৈরি হতে কয়েক বছর লেগেছে। Ninja-তে প্রয়োজনীয় dynamic dependency সমর্থন আগেই এসেছিল, আর CMake ৩.২৮ (২০২৩-এর শেষে) থেকে C++20 modules-এর সমর্থন আর পরীক্ষামূলক নয় — Ninja আর Visual Studio generator-এ, পর্যাপ্ত নতুন compiler-এর সাথে। import std;-এর সমর্থন আরও নতুন আর এখনো পরিপক্ব হচ্ছে।

মূল উপলব্ধি: header-only বনাম compiled হলো সুবিধা আর compile সময়ের বিনিময়; modules সেই বিনিময় কমাতে পারে, কিন্তু বিনিময়ে build system-কে আরও বুদ্ধিমান হতে হয়। ভাষার একটি feature কতটা কাজের, তা অনেক সময় নির্ভর করে তার চারপাশের টুল কতটা তৈরি।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. পুরোনো থেকে আধুনিক: একটি AI এই রেসিপি দিয়েছে। প্রতিটি লাইনের সমস্যা বলুন, আর modern রূপ লিখুন।
    cmake_minimum_required(VERSION 2.8)
    project(shop)
    set(CMAKE_CXX_FLAGS "-std=c++17 -Wall -O2")
    include_directories(include)
    file(GLOB SRC src/*.cpp)
    add_executable(shop ${SRC} apps/main.cpp)

    সমস্যা: ২.৮ খুব পুরোনো (CMake ৪.০-তে error); project-এ ভাষা নেই; CMAKE_CXX_FLAGS-এ compiler-নির্দিষ্ট flag আর ভুল standard, MSVC-তে ভাঙবে, -O2 build type-এর সাথে লড়বে; global include_directories; file(GLOB) নতুন ফাইল বাদ দেবে; library নেই, তাই টেস্ট আলাদা করে link করা যাবে না। আধুনিক রূপ:

    cmake_minimum_required(VERSION 3.25)
    project(shop LANGUAGES CXX)
    add_library(shop_core STATIC src/inventory.cpp src/pricing.cpp)
    target_include_directories(shop_core PUBLIC include)
    target_compile_features(shop_core PUBLIC cxx_std_23)
    add_executable(shop apps/main.cpp)
    target_link_libraries(shop PRIVATE shop_core)

    warning-এর জন্য অংশ ৭-এর if(MSVC) অংশটি যোগ করুন, আর optimization দিন CMAKE_BUILD_TYPE বা preset দিয়ে।

  2. কী আবার build হবে? একটি প্রজেক্টে inventory.h include করে inventory.cpp, report.cpp আর main.cpp; pricing.cpp করে না। সবগুলো একটি static library-তে, শুধু main.cpp প্রোগ্রামে। (ক) inventory.h-এ একটি comment বদলালেন। (খ) pricing.cpp-এ একটি লাইন বদলালেন। প্রতিটি ক্ষেত্রে কোন কোন ধাপ চলবে?

    (ক) inventory.cpp, report.cpp, main.cpp — তিনটিই আবার compile (comment হলেও; build টুল শুধু দেখে ফাইলটি নতুন কি না, ভেতরের অর্থ বোঝে না), তারপর library আবার জোড়া, তারপর প্রোগ্রাম link. pricing.cpp বাঁচে। (খ) শুধু pricing.cpp compile, library জোড়া, প্রোগ্রাম link — মোট তিন ধাপ।

  3. PUBLIC না PRIVATE? shop_core-এর জন্য প্রতিটি নির্ভরতা কোন শব্দে link হবে? (ক) Threads::Threads, যা শুধু inventory.cpp-এর ভেতরে একটি background thread চালাতে লাগে। (খ) shop_money, কারণ include/shop/pricing.h-এর ফাংশন Taka ফেরত দেয়। (গ) একটি JSON library, যা report.cpp-তে ফাইল লিখতে লাগে, আর কোনো public header-এ আসে না।

    (ক) PRIVATE — শুধু .cpp-এর ভেতরে। (খ) PUBLIC — public header-এ টাইপটি আসে, তাই ব্যবহারকারীকেও shop_money-এর header দেখতে হবে। (গ) PRIVATE. মনে রাখুন, shop_core static হলে (ক) আর (গ)-র library-ও link-এর সময় প্রোগ্রামে যোগ হবে, কিন্তু CMake সেটা নিজেই সামলায়; আপনি শুধু ঠিক শব্দটি দিন।

  4. নিজে চালিয়ে দেখুন (কমান্ড): অংশ ৬-এর প্রজেক্টটি নিজের মেশিনে বানান। তারপর একই source থেকে দুটি build বানান — build-debug (Debug) আর build-release (Release) — আর দুটোতে অংশ ৭-এর build_mode.cpp-এর মতো একটি NDEBUG-যাচাই প্রোগ্রাম চালিয়ে আউটপুট মেলান। কোন কমান্ড লাগবে?
    cmake -S . -B build-debug   -G Ninja -DCMAKE_BUILD_TYPE=Debug
    cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release
    cmake --build build-debug
    cmake --build build-release
    ./build-debug/shop_cli      # Debug-style ... assert is ON
    ./build-release/shop_cli    # Release-style ... assert is OFF

    (ধরে নেওয়া হয়েছে, build_mode.cpp-এর #ifdef NDEBUG অংশটি আপনি shop_cli-এর main-এ যোগ করেছেন।) দুটি ফোল্ডার সম্পূর্ণ স্বাধীন; একটির build অন্যটিকে ছোঁয় না — out-of-source build-এর সুবিধা। Visual Studio generator-এ একটি ফোল্ডারই যথেষ্ট, আর cmake --build build --config Release দিয়ে বাছবেন। এই কাজ বারবার করতে হলে অংশ ৭-এর মতো দুটি preset বানিয়ে নিন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি নতুন CMake প্রজেক্টে প্রথমবার build আর টেস্ট করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. CMakeLists.txt-এ target আর তাদের নির্ভরতা লেখা
  2. Configure: CMake compiler খুঁজে CMakeCache.txt লেখে
  3. Generate: CMake build.ninja বা Makefile লেখে
  4. Build: build টুল প্রতিটি .cpp compile করে link করে
  5. ctest দিয়ে টেস্ট চালানো

CMake-এর মূল কাজ কোনটি?

  • C++ কোড compile করে machine code বানানো
  • প্ল্যাটফর্ম-নিরপেক্ষ রেসিপি থেকে Ninja, Makefile বা Visual Studio-র build ফাইল তৈরি করা
  • Object file জুড়ে executable বানানো
  • একমাত্র অফিসিয়াল C++ package manager হিসেবে library বসানো

একটি library core-এর public header-এ অন্য library money-র একটি header #include করা আছে। CMake-এ core কীভাবে money-কে link করবে?

  • target_link_libraries(core PRIVATE money)
  • target_link_libraries(core INTERFACE money)
  • target_link_libraries(core PUBLIC money)
  • include_directories(money/include)

Ninja দিয়ে build হওয়া একটি প্রজেক্টে শুধু একটি .cpp ফাইল বদলালে পরের build-এ সাধারণত কী ঘটে?

  • শুধু সেই ফাইলের object file আবার compile হয়, তারপর দরকারি link
  • প্রজেক্টের সব .cpp আবার compile হয়
  • প্রথমে পুরো configure আবার চলে, তারপর সব build
  • শুধু compile হয়, link-এর দরকার পড়ে না

শুধু header নিয়ে তৈরি, কোনো .cpp নেই এমন library-র জন্য CMake-এ সাধারণত কোন ধরনের target বানানো হয়?

  • add_library(x STATIC)
  • add_library(x SHARED)
  • add_executable(x)
  • add_library(x INTERFACE)

CMake-এ -DCMAKE_BUILD_TYPE=Release দিয়ে GCC-তে build করা প্রোগ্রামে assert(stock >= 0)-এর কী হয়?

  • শর্ত মিথ্যা হলে প্রোগ্রাম থেমে বার্তা দেখায়
  • NDEBUG সংজ্ঞায়িত থাকে, তাই assert মুছে যায় আর শর্ত যাচাই হয় না
  • শর্ত মিথ্যা হলে compile error হয়
  • শর্ত মিথ্যা হলে std::logic_error ছোড়া হয়

CMake-এ source ফাইল জোগাড় করতে file(GLOB SRC src/*.cpp) ব্যবহার নিয়ে প্রধান সমস্যা কোনটি?

  • এটি compile অনেক ধীর করে দেয়
  • এটি শুধু Linux-এ কাজ করে
  • নতুন .cpp যোগ করলে CMake আবার না চালানো পর্যন্ত সেটি build-এ বাদ থাকে
  • এটি header ফাইলগুলোকেও আলাদা করে compile করায়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. CMake রেসিপি লেখে, রান্না করে না। সে build system generator; আসল compile আর link চালায় Ninja, Make বা MSBuild.
  2. দুই কমান্ড, আলাদা ফোল্ডার: cmake -S . -B build (configure + generate), cmake --build build (build). কখনো in-source নয়।
  3. Build টুল একটি graph দেখে — একটি .cpp বদলালে শুধু তার object আর link; একটি header বদলালে তার সব ব্যবহারকারী।
  4. Modern CMake = target + property. PRIVATE (শুধু আমি), PUBLIC (আমি + ব্যবহারকারী), INTERFACE (শুধু ব্যবহারকারী)। global include_directories আর CMAKE_CXX_FLAGS নয়।
  5. চারপাশের যন্ত্র: Debug/Release/RelWithDebInfo, প্রতি target-এ warning, option-এর পেছনে sanitizer, find_package/FetchContent/vcpkg, presets, compile_commands.json, ctest.

পরের পর্বে: প্রজেক্ট এখন build হয়, আর ctest চালানোর জায়গাও তৈরি। কিন্তু টেস্টে কী লিখবেন, ভালো টেস্ট দেখতে কেমন, আর টেস্ট ফেল করলে debugger দিয়ে কীভাবে কারণ খুঁজবেন? পর্ব ৪৮-এ — টেস্টিং ও ডিবাগিং।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান