Phần 1 viết hướng dẫn makefile đơn giản nhất nằm trong series Makefile cơ bản mà mình hiện đang làm, mọi thứ đều bắt nguồn từ câu hỏi Makefile là gì ? nằm trong bài viết cũng lâu lâu lắm rồi, trước năm 75 thì phải 😀

Make được dùng để tự động chuyển đổi mã nguồn thành một tệp thực thi. Ưu điểm của make script là bạn có thể chỉ định mối liên quan của các thành phần trong chương trình của mình qua make, và make sẽ biết cái nào liên quan tới cái nào và khi nào cần thực hiện những thứ đó. Với việc sử dụng thông tin này, sẽ giúp tối ưu hóa quá trình build và tránh các bước không cần thiết.

Makefile cơ bản

Thông thường với make thì sẽ thực thi các lệnh được lưu trong 1 file gọi là Makefile. Dưới đây là một Makefile cơ bản nhất cho chương trình helloworld.c

#include <stdio.h>
 
main( int argc, char *argv[] )
{
    printf( "Hello, world\n" );
}

Makefile

hello: hello.c
    gcc hello.c -o hello
clean:
    rm -f *.o hello

Sau khi chạy thử và thực thi chương trình sẽ có kết quả như sau

$ make
gcc hello.c -o hello
$ ./hello
Hello, world

Mục đích chính của makefile là build chương trình, nếu target output có dạng như gcc hello.c -o hello nghĩa là chương trình chưa được build hoặc có update mới.

Thông thường source code thường là không hoàn chỉnh và phải được tạo ra từ những công cụ như flex hoặc bison,sau đó nó sẽ được compile thành binary object(file .o). Sau đó, đối với C / C ++, các tệp đối tượng được liên kết với nhau bằng một trình liên kết (linker) (thường được gọi thông qua trình biên dịch, gcc) để tạo thành một chương trình thực thi.

Targets và Prerequisite

Một makefile cần thiết thì sẽ bao gồm nhiều rule để build ứng dụng. Rule đơn giản nhất gọi là rule mặc định(default rule), rule này gồm 3 phần: mục tiêu(target), điều kiện(prerequisite) và command thực hiện

target: prereq1 prereq2
     commands
  • Target: file bạn cần make
  • prerequisites hay dependent: là những file cần phải tồn tại trước khi target được tạo thành công
  • command: là shell command tạo ra target từ prerequisites

Ví dụ:

foo.o: foo.c foo.h
    gcc -c foo.c

Target ở đây là foo.o nằm trước dấu hai chấm, prerequisites là foo.c và foo.h, command là gcc -c foo.c, trước command là tab chứ không phải là space nha anh em

Tiếp theo là một ví dụ đếm số chữ trong các từ “fee,” “fie,” “foe,” và “fum” trong input. Chương trình này sử dụng flex scanner trong chương trình main

Flex scanner là gì ? Dưới đây là nguyên văn tiếng anh mô tả của nó tại Ubuntu, hiểu đơn giản thì nó là chương trình gen code c và scan input, đó cũng là mục đích chính của việc dùng tool này

flex is a tool for generating scanners: programs which recognized lexical patterns in text. flex reads the given input files, or its standard input if no file names are given, for a description of a scanner to generate. The description is in the form of pairs of regular expressions and C code, called rules. flex generates as output a C source file, lex.yy.c, which defines a routine yylex(). This file is compiled and linked with the -lfl library to produce an executable. When the executable is run, it analyzes its input for occurrences of the regular expressions. Whenever it finds one, it executes the corresponding C code.

Để cài đặt flex ubuntu dùng 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 %d\n", 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 -ocount_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

Kết quả sau khi thực thi Makefile, sau khi run chương trình thực thi ta thấy các từ “fee,” “fie,” “foe,” và “fum” mỗi từ đề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 -ocount_words
$ ./run-run
$ count_words < lexer.l
        int _count = 0;
        int _count = 0;
        int _count = 0;
        int _count = 0;
%%
        _count++;
        _count++;
        _count++;
        _count++;
3 3 3 3

Kiểm tra Dependency

Makefile ở chương trình trên có khá nhiều mục và đều liên quan với nhau, làm sao để make biết phải làm gì ? kiểm tra tính phụ thuộc(dependency) là sao ?

Trước tiên hãy lưu ý rằng command không chứa target để nó quyết định make default goal là count_words. Nó kiểm tra các prerequisite và thấy có ba điều kiện: count_words.o, lexer.o-lfl. bây giờ hãy xem xét cách make count_words.o và rule cho nó Tiếp tục, nó kiểm tra prerequisites, thông báo rằng count_words.c không có quy tắc nào ngoài việc tệp tồn tại, do đó, make chuyển đổi count_words.c thành count_words.o bằng cách thực hiện lệnh: gcc -c count_words.c

Prerequisite tiếp theo mà make cần phải xem xét là lexer.o. Rule của nó được thực thi trên file lexer.c, tuy nhiên file này không tồn tại, make sẽ tìm rule để tạo ra file này từ lexer.l, do đó nó sẽ run chương trình flex. Khi lexer.c được tạo ra nó có thể run gcc command

Cuối cùng make sẽ thực thi -lfl, -l ở đây có nghĩa là thư viện được link tới ứng dụng. Thư viện chính cần link ở đây là fl(libfl.a), GNU make hỗ trợ syntax -l<NAME>, khi make thấy lờ thì nó sẽ đi tìm cờ, cờ ở đây là file có dạng libNAME.so, nếu tìm k thấy file này nó sẽ tìm file libNAME.a, khi đã tìm ra thì công việc cuối cùng là linking.

Tối ưu output sau build

Sau khi run chương trình ngoài đoạn output cần thiết là 3 3 3 3 thì ta thấy còn build thêm mấy đoạn code thừa từ lexer, đây là thứ mà chúng ta hoàn toàn không mong muốn. Lỗi ở đây là do chúng ta quên một số rule trong flex. Để khắc phục tình trạng này thì ta cần add thêm \n như sau

    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++;
.
\n

Sau khi xong 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
$ ./run-run
$ count_words < lexer.l
3 3 3 3

Tạm kết

Bây giờ bạn đã có một sự hiểu biết cơ bản về make và có thể viết makefile của riêng mình. Ở đây mình đã cố gắng nói đủ cú pháp và cấu trúc của makefile cơ bản nhất để bạn có thể bắt đầu sử dụng make. Tất nhiên sẽ có phần nói rõ hơn về rule, make variable,.. ở bài tiếp theo. Anh em bình tĩnh hóng nhé.