Phần 1 của series Makefile cơ bản đang trong quá trình được thực hiện, bắt nguồn từ câu hỏi “Makefile là gì?” đã được đề cập trong bài viết lâu đời. Trước năm 1975, chúng ta có thể nói là thật sự đã lâu đấy 😀
Makefile được sử dụng để tự động biên dịch và tạo ra chương trình thực thi từ mã nguồn. Lợi ích của Makefile là ta có thể chỉ định sự phụ thuộc giữa các thành phần trong chương trình thông qua make, và make sẽ biết được những thành phần nào liên quan với nhau và khi nào cần thực hiện những công việc đó. Với việc sử dụng thông tin này, quá trình biên dịch và xây dựng sẽ được tối ưu hơn và tránh các bước không cần thiết.
Bạn đang xem: Hướng dẫn viết Makefile đơn giản
Makefile cơ bản
Thường thì chúng ta sẽ thực hiện các lệnh của make thông qua một file gọi là Makefile. Dưới đây là một ví dụ cơ bản nhất của Makefile cho chương trình helloworld.c
#include <stdio.h> int main( int argc, char *argv[] ) { printf( "Hello, world!n" ); }
Nội dung của Makefile
hello: hello.c gcc hello.c -o hello clean: rm -f *.o hello
Sau khi chạy và thực thi Makefile, kết quả nhận được sẽ là
$ make gcc hello.c -o hello $ ./hello Hello, world!
Mục đích chính của Makefile là xây dựng chương trình. Nếu tập tin đầu ra có dạng như “gcc hello.c -o hello” thì có nghĩa là chương trình chưa được xây dựng hoặc đã có sự cập nhật mới.
Thường thì mã nguồn không hoàn chỉnh và cần được tạo ra từ các công cụ như flex hoặc bison, sau đó nó sẽ được biên dịch thành các tệp nhị phân (.o). Tiếp theo, đối với C/C++, các tệp nhị phân này sẽ được liên kết với nhau bằng trình liên kết (linker) (thông qua trình biên dịch, thường là gcc) để tạo ra chương trình thực thi.
Mục tiêu và điều kiện tiên quyết
Một Makefile cần có nhiều quy tắc để xây dựng ứng dụng. Quy tắc cơ bản nhất được gọi là quy tắc mặc định (default rule), quy tắc này gồm ba phần: mục tiêu (target), điều kiện tiên quyết (prerequisite) và lệnh thực hiện
target: prereq1 prereq2 commands
- Mục tiêu (target): tập tin mà bạn muốn xây dựng
- Điều kiện tiên quyết (prerequisites): các tập tin cần tồn tại trước khi mục tiêu được xây dựng thành công
- Lệnh (commands): lệnh shell để tạo ra mục tiêu từ các điều kiện tiên quyết
Ví dụ:
foo.o: foo.c foo.h gcc -c foo.c
Mục tiêu ở đây là foo.o (được đặt trước hai dấu hai chấm), điều kiện tiên quyết là foo.c và foo.h, lệnh là gcc -c foo.c. Lưu ý rằng trước lệnh phải có một tab chứ không phải khoảng trắng.
Tiếp theo là một ví dụ đếm số chữ trong các từ “fee,” “fie,” “foe,” và “fum” trong một đầu vào. Chương trình này sử dụng scanner flex trong chương trình chính
Flex scanner là gì? Dưới đây là mô tả nguyên bản tiếng Anh của nó trên Ubuntu, tóm tắt đơn giản thì đó là một công cụ để tạo ra mã nguồn C và quét đầu vào, điều này cũng là mục đích chính của việc sử dụng công cụ này
flex là một công cụ để tạo ra scanner: các chương trình nhận dạng các mẫu từ vựng trong văn bản. flex đọc các tệp đầu vào đã cho, hoặc đầu vào tiêu chuẩn nếu không có tên tệp nào được cung cấp, để tạo ra một mô tả của scanner cần tạo. Mô tả này có dạng các cặp biểu thức chính quy và mã C, gọi là quy tắc. flex tạo ra một tệp nguồn C, lex.yy.c, định nghĩa một hàm yylex(). Tệp này được biên dịch và liên kết với thư viện -lfl để tạo ra một tập tin thực thi. Khi tệp thực thi được chạy, nó phân tích cú pháp của đầu vào để tìm các chuỗi phù hợp với biểu thức chính quy. Mỗi khi tìm thấy một chuỗi phù hợp, nó thực hiện mã C tương ứng.
Để cài đặt flex trên Ubuntu, chạy các lệnh sau
sudo apt-get update sudo apt-get install flex
Chương trình chính count_word.c
#include <stdio.h> #include <stdlib.h> extern int fee_count, fie_count, foe_count, fum_count; extern int yylex(void); int main(int argc, char **argv) { yylex(); printf("%d %d %d %dn", fee_count, fie_count, foe_count, fum_count); exit(0); }
Chương trình scanner lexer.l
int fee_count = 0; int fie_count = 0; int foe_count = 0; int fum_count = 0; %% fee fee_count++; fie fie_count++; foe foe_count++; fum fum_count++; .
Makefile
count_words: count_words.o lexer.o -lfl gcc count_words.o lexer.o -lfl -o count_words count_words.o: count_words.c gcc -c count_words.c lexer.o: lexer.c gcc -c lexer.c lexer.c: lexer.l flex -t lexer.l > lexer.c clean: rm -f *.o lexer.c count_words
Sau khi thực thi Makefile, sau khi chạy chương trình có thể thấy rằng các từ “fee,” “fie,” “foe,” và “fum” đều có 3 chữ cái nên kết quả sẽ là 3 3 3 3
$ make gcc -c count_words.c flex -t lexer.l > lexer.c gcc -c lexer.c gcc count_words.o lexer.o -lfl -o count_words $ ./count_words < lexer.l 3 3 3 3
Kiểm tra phụ thuộc
Makefile trong ví dụ trên có nhiều mục và đều có liên quan đến nhau, nhưng làm thế nào để make biết phải làm gì? Làm thế nào để kiểm tra tính phụ thuộc (dependency)?
Đầu tiên, hãy lưu ý rằng lệnh không chứa mục tiêu, điều này khiến make quyết định mục tiêu mặc định là count_words. Make kiểm tra các điều kiện tiên quyết và tìm thấy ba điều kiện: count_words.o, lexer.o và -lfl. Bây giờ, hãy xem cách make xây dựng count_words.o và quy tắc cho nó. Tiếp theo, nó kiểm tra các điều kiện tiên quyết, thông báo rằng không có quy tắc ngoại trừ việc tập tin count_words.c tồn tại, do đó make biên dịch count_words.c thành count_words.o bằng cách thực hiện lệnh gcc -c count_words.c
Điều kiện tiên quyết tiếp theo mà make cần xem xét là lexer.o. Quy tắc của nó sẽ thực hiện trên tập tin lexer.c, tuy nhiên tập tin này không tồn tại, vì vậy make sẽ tìm cách tạo tập tin này từ lexer.l, sau đó nó sẽ chạy chương trình flex. Khi lexer.c được tạo ra, gcc được chạy để biên dịch tập tin này.
Cuối cùng, make sẽ thực hiện -lfl, -l ở đây có nghĩa là liên kết với thư viện. Thư viện chính cần liên kết ở đây là fl(libfl.a), GNU make hỗ trợ cú pháp -l<TÊN>, khi make thấy lệnh này, nó sẽ tìm file tên lib<TÊN>.so, nếu không tìm thấy thì nó sẽ tìm file lib<TÊN>.a, khi tìm ra thì công việc cuối cùng là liên kết.
Tối ưu kết quả sau quá trình xây dựng
Sau khi chạy chương trình, ngoài đầu ra mong đợi là 3 3 3 3, ta còn thấy có một số mã thừa từ lexer, điều này là điều ta không mong muốn. Lỗi ở đây là ta đã bỏ qua một số phần quy tắc trong flex. Để khắc phục vấn đề này, ta cần thêm dòng này
.
Sau khi hoàn thành, chúng ta sẽ có kết quả như sau
$ make gcc -c count_words.c flex -t lexer.l > lexer.c gcc -c lexer.c gcc count_words.o lexer.o -lfl -o count_words $ ./count_words < lexer.l 3 3 3 3
Tạm kết
Bây giờ bạn đã có kiến thức cơ bản về make và có thể viết Makefile riêng của mình. Ở đây, chúng tôi đã giới thiệu cú pháp và cấu trúc cơ bản nhất của Makefile để bạn có thể bắt đầu sử dụng make. Đương nhiên sẽ có thêm thông tin về quy tắc, biến make,.. trong bài viết tiếp theo. Hãy chờ đợi nhé.