Hướng dẫn CMake cho người mới bắt đầu qua các ví dụ thực tế

CMake là một công cụ mạnh mẽ giúp đơn giản hóa quá trình xây dựng (build) các dự án C++ đa nền tảng. Bài viết này sẽ giới thiệu CMake thông qua các ví dụ thực tế, dễ hiểu, giúp bạn nhanh chóng làm quen và áp dụng vào dự án của mình. Các ví dụ được thực hiện trên Ubuntu, nhưng hoàn toàn có thể áp dụng tương tự trên các hệ điều hành khác.

Tiện ích make và các Makefile cung cấp một hệ thống build, giúp quản lý việc biên dịch (compile) và tái biên dịch (re-compilation) chương trình viết bằng nhiều ngôn ngữ khác nhau. Tuy nhiên, việc sử dụng Makefile đôi khi trở nên phức tạp khi dự án có nhiều thư mục con (sub-directory) hoặc cần triển khai trên nhiều nền tảng khác nhau.

Để giải quyết vấn đề này, CMake ra đời. CMake là một công cụ tạo Makefile đa nền tảng. Hiểu đơn giản, CMake tự động tạo ra Makefile cho dự án của bạn. Ngoài ra, CMake còn có nhiều tính năng khác, nhưng trong bài viết này, chúng ta sẽ tập trung vào khả năng tự động sinh Makefile cho các dự án C/C++.

Ví dụ 1: Hello World

Đoạn mã cho ví dụ này có thể tìm thấy trong thư mục. Trong ví dụ này, một chương trình Hello World đơn giản sẽ được xây dựng (HelloWorld.cpp):

#include <iostream>

int main(int argc, char *argv[]){
    std::cout << "Hello World!" << std::endl;
    return 0;
}

Ngoài file HelloWorld.cpp, chúng ta cần một file CMakeLists.txt ở cùng thư mục với nội dung như sau:

cmake_minimum_required(VERSION 2.8.9)
project (hello)
add_executable(hello HelloWorld.cpp)

File này chỉ có 3 dòng, ý nghĩa của từng dòng như sau:

  • Dòng đầu tiên định nghĩa phiên bản tối thiểu của CMake mà dự án này yêu cầu.
  • Dòng thứ hai sử dụng lệnh project() để đặt tên cho dự án.
  • Dòng thứ ba là lệnh add_executable(). Lệnh này tạo một file thực thi (executable). Đối số đầu tiên là tên của file thực thi sẽ được tạo, đối số thứ hai là file mã nguồn (source file) được sử dụng để xây dựng file thực thi.

Để build dự án, hãy chắc chắn rằng CMake đã được cài đặt trên máy tính của bạn. Nếu bạn đang sử dụng một bản phân phối Linux như Ubuntu, bạn có thể cài đặt CMake thông qua trình quản lý gói (package manager):

sudo apt-get install cmake
cmake --version

Điều hướng terminal đến thư mục chứa mã nguồn dự án và kiểm tra xem đã có đủ 2 file trên chưa:

cd ~/Workspaces/Examples/exploringBB/extras/cmake/helloworld/
ls

Bây giờ, chúng ta đã sẵn sàng để build dự án HelloWorld bằng CMake. Thực thi lệnh cmake với đường dẫn đến thư mục chứa mã nguồn và file CMakeLists.txt. Trong trường hợp này, “.” trỏ đến thư mục hiện tại:

cmake .

CMake sẽ xác định cấu hình môi trường trên máy tính của bạn và tạo một file Makefile cho dự án này. Bạn có thể xem và chỉnh sửa Makefile này, nhưng mọi thay đổi sẽ bị ghi đè mỗi khi bạn chạy lại lệnh cmake.

Sau khi Makefile đã được tạo, sử dụng lệnh make để build dự án:

make

Dự án đã được build thành công! Quá trình build chương trình HelloWorld.cpp có vẻ hơi phức tạp, nhưng nó rất quan trọng cho người mới bắt đầu vì nó giải thích các hoạt động cơ bản của CMake. Bây giờ, hãy xem xét một số ví dụ CMake phức tạp hơn.

Ví dụ 2: Dự án với nhiều thư mục

Khi dự án lớn dần, việc quản lý mã nguồn bằng nhiều thư mục con (sub-directory) trở nên cần thiết. Việc sử dụng Makefile trở nên phức tạp khi có sự hiện diện của các thư mục con, vì thông thường, cần tạo một Makefile trong mỗi thư mục con. Các Makefile này sau đó được gọi bởi Makefile trong thư mục gốc.

CMake sẽ rất hữu ích trong trường hợp này. Ví dụ này sử dụng một dự án với cấu trúc thư mục điển hình. Điều hướng terminal đến thư mục /exploringBB/extras/cmake/student có cấu trúc như sau:

.
├── build
├── CMakeLists.txt
├── include
│   └── Student.h
└── src
    ├── main.cpp
    └── Student.cpp

3 directories, 4 files

Như bạn thấy, tất cả các file header (.h) được đặt trong thư mục include, và tất cả các file mã nguồn (.cpp) được đặt trong thư mục src. Ngoài ra, có một thư mục build (hiện đang rỗng) để chứa các file binary thực thi và các file tạm thời cần thiết cho quá trình build. File CMakeLists.txt cho dự án này sẽ có một chút khác biệt so với file trong ví dụ 1:

cmake_minimum_required(VERSION 2.8.9)
project(directory_test)
include_directories(include)
#set(SOURCES src/main.cpp src/Student.cpp)
file(GLOB SOURCES "src/*.cpp")
add_executable(testStudent ${SOURCES})

Các thay đổi quan trọng trong file CMake này:

  • Hàm include_directories() được sử dụng để tích hợp các file header vào môi trường build.
  • Hàm set(SOURCE...) có thể được sử dụng để đặt một biến (SOURCE) chứa tất cả tên của các file source (.cpp) trong dự án. Tuy nhiên, vì mỗi file source cần được thêm thủ công, dòng tiếp theo sử dụng một cách tiếp cận khác, và hàm set được comment lại.
  • Hàm file() được sử dụng để thêm file nguồn vào dự án. GLOB (hoặc GLOB_RECURSE) được sử dụng để tạo một danh sách các file thỏa mãn biểu thức đã khai báo (ví dụ: src/*.cpp) và thêm chúng vào biến SOURCE.
  • Hàm add_executable() sử dụng biến SOURCE thay vì tham chiếu cụ thể đến từng file nguồn để build một chương trình thực thi là testStudent.

Trong ví dụ này, tất cả các file build sẽ được đặt trong thư mục build. Bạn có thể thực hiện việc này dễ dàng bằng cách gọi cmake từ thư mục build:

cd build
cmake ..

Thư mục build lúc này sẽ bao gồm Makefile cho dự án, Makefile sẽ tham chiếu chính xác đến các file trong thư mục srcinclude. Dự án có thể được build từ thư mục build bằng lệnh make:

make

Một ưu điểm của cách tiếp cận này là tất cả các file liên quan đến quá trình build đều nằm trong thư mục build. Để dọn dẹp (clean) dự án, bạn chỉ cần xóa đệ quy tất cả các file/thư mục trong thư mục build, ví dụ:

cd ..
rm -r build/*

Cấu trúc cây thư mục của dự án sẽ trở lại trạng thái ban đầu trước khi chạy cmake.

Lưu ý: Mỗi khi thêm file nguồn mới vào dự án, bạn cần chạy lại cmake để cập nhật Makefile cho những thay đổi mới.

Ví dụ 3: Xây dựng một Shared Library

Shared library (thư viện được chia sẻ) là một thư viện được liên kết động (dynamically linked) với chương trình khi chạy. Trong ví dụ này, một shared library sẽ được xây dựng sử dụng mã nguồn từ ví dụ 2. Cấu trúc dự án tương tự ví dụ 2, nhưng file main.cpp bị loại bỏ vì nó không liên quan đến việc build thư viện. Shared library sẽ chỉ chứa một class Student. Cấu trúc thư mục của dự án như sau:

.
├── build
├── CMakeLists.txt
├── include
│   └── Student.h
└── src
    └── Student.cpp

3 directories, 3 files

Tương tự, file header được đặt trong thư mục include, và file mã nguồn được đặt trong thư mục src. Thư mục build rỗng được sử dụng để chứa file lib ở dạng binary và các file tạm thời cần thiết cho quá trình build. File CMakeLists.txt có nội dung như sau:

cmake_minimum_required(VERSION 2.8.9)
project(directory_test)
set(CMAKE_BUILD_TYPE Release)
include_directories(include)
file(GLOB SOURCES "src/*.cpp")
add_library(testStudent SHARED ${SOURCES})
install(TARGETS testStudent DESTINATION /usr/lib)

Các thay đổi quan trọng trong file này:

  • Hàm set(CMAKE_BUILD_TYPE Release) được sử dụng để đặt kiểu build thành Release.
  • Thay vì sử dụng hàm add_executable() như trong ví dụ trước, ví dụ này sử dụng hàm add_library(). Thư viện được xây dựng là một shared library, vì vậy chúng ta set flag là SHARED (ngoài ra còn có các tùy chọn khác là STATIC hoặc MODULE), và testStudent là tên được sử dụng cho shared library này.
  • Dòng cuối cùng sử dụng hàm install() để định nghĩa vị trí cài đặt cho lib (trong trường hợp này là /usr/lib). Việc triển khai (deploy) trong trường hợp này được kích hoạt bằng cách gọi sudo make install.

Trong ví dụ này, lib được build trong thư mục build với kết quả như sau:

cd build
cmake ..
make
ls -l *.so

Chúng ta có thể sử dụng lệnh ldd để hiển thị các dependencies của shared library:

ldd libtestStudent.so

File CMakeLists.txt còn bao gồm cả bước triển khai, cho phép chúng ta cài đặt lib ở một vị trí thuận lợi cho việc truy cập. Vị trí của shared library có thể được thêm vào path, hoặc nếu muốn nó khả dụng ở mức toàn hệ thống (system wide), chúng ta có thể thêm chúng vào thư mục /usr/lib. Ví dụ, lib libtestStudent.so có thể được cài đặt system wide bằng cách sau:

sudo make install

Bước này cần được thực hiện với quyền root để có thể ghi vào thư mục /usr/lib. Bạn có thể tìm thấy file cmake_install.cmake trong thư mục build, mô tả vị trí mà lệnh make install thực hiện những thay đổi.

Ví dụ 4: Xây dựng một Static Library

Static library (thư viện tĩnh) được tạo tại thời điểm biên dịch bằng cách tổng hợp toàn bộ mã nguồn liên quan đến thư viện. Về bản chất, nó sẽ sao chép mã nguồn của các dependency vào một thư viện khác. Điều này dẫn đến việc kích thước của static library thường lớn hơn shared library, nhưng vì tất cả các dependency đều được xác định tại thời điểm biên dịch, chi phí cho việc loading tại thời điểm runtime sẽ ít hơn, và thư viện ít phụ thuộc hơn vào nền tảng (platform) mà nó đang chạy. Trong phần lớn các trường hợp, nên sử dụng shared library trừ khi bắt buộc phải dùng static library, vì nó hạn chế được việc trùng lặp mã nguồn và shared library có thể được cập nhật mà không cần recompile.

Để xây dựng một static library bằng CMake, các bước gần như giống hệt ví dụ 3. Code cho ví dụ này có thể tìm thấy ở đây, và file CMakeLists.txt có nội dung như sau:

cmake_minimum_required(VERSION 2.8.9)
project(directory_test)
set(CMAKE_BUILD_TYPE Release)
include_directories(include)
file(GLOB SOURCES "src/*.cpp")
add_library(testStudent STATIC ${SOURCES})
install(TARGETS testStudent DESTINATION /usr/lib)

Sử dụng các bước tương tự như trên để xây dựng một static library:

cd build
cmake ..
make
ls -l lib*

Chúng ta có thể xác định được các thành phần cấu tạo nên static library bằng cách sử dụng lệnh ar trong GNU:

ar -t libtestStudent.a

Ngoài ra, chúng ta có thể sử dụng lệnh nm trong GNU để liệt kê tất cả các ký tự trong file object và binary. Trong trường hợp này, lệnh này sẽ liệt kê các ký tự trong thư viện student và kiểu của nó (ví dụ: T là code, U là không xác định, R là dữ liệu read-only). Thông tin này rất hữu dụng cho việc debug các vấn đề có thể xảy ra với static library.

nm -C libtestStudent.a

Ví dụ 5: Sử dụng một Shared hoặc Static Library

Một khi thư viện đã được tạo (ví dụ 3 và ví dụ 4), câu hỏi tiếp theo là làm thế nào để sử dụng các thư viện này trong dự án của chúng ta? CMake có thể được sử dụng để sinh ra Makefile trong dự án của chúng ta, giúp đơn giản hóa việc này.

Dưới đây là nội dung của file CMakeLists.txt có thể được sử dụng để build một chương trình liên kết (link) đến lib (shared lib hoặc static lib). Trong ví dụ này, shared lib được sinh ra trong ví dụ 3 sẽ được sử dụng, và một chương trình C++ ngắn được viết để sử dụng các chức năng của lib đó. Nội dung của đoạn mã ngắn này cũng được trình bày phía dưới:

cmake_minimum_required(VERSION 2.8.9)
project (TestLibrary)

#For the shared library:
set ( PROJECT_LINK_LIBS testStudent )
link_directories( ~/exploringBB/extras/cmake/studentlib_shared/build )

#For the static library:
#set ( PROJECT_LINK_LIBS libtestStudent.a )
#link_directories( ~/exploringBB/extras/cmake/studentlib_static/build )

include_directories(~/exploringBB/extras/cmake/studentlib_shared/include)
add_executable(libtest main.cpp)
target_link_libraries(libtest ${PROJECT_LINK_LIBS} )
#include <iostream>
#include "Student.h"

int main(int argc, char *argv[]){
    Student s("Joe");
    s.display();
    return 0;
}

Dự án có thể được build và chạy theo các bước dưới đây. Nếu quá trình build thất bại, hãy kiểm tra xem trong CMakeLists.txt đường dẫn đến shared lib đã đúng chưa:

cd build
cmake ..
make
./libtest

Lời kết

Các ví dụ trên hy vọng đã mang lại cho bạn cái nhìn ngắn gọn và dễ hiểu về công dụng và cách sử dụng cơ bản của CMake. Để tìm hiểu sâu hơn về CMake và cập nhật những thông tin mới nhất, bạn có thể ghé qua trang chủ của CMake. Hẹn gặp lại bạn trong những bài đăng sau!

Nguồn: https://sentayho.com.vn/hello-world-introductions-to-cmake/#Source_Code_for_this_Discussion