Sử dụng Makefile và tiện ích make để build mã nguồn C/C++

From CodeForLife
Jump to: navigation, search

Thông thường, khi build một số project đơn giản, chỉ có một vài tệp mã nguồn bạn có thể dùng trực tiếp trình dịch như gcc, g++, cc, c++ để build. Nhưng với một project lớn và chia làm nhiều module thì không khả thi khi build bằng lệnh trực tiếp. Bạn cũng có thể dùng các IDE hỗ trợ để tạo project, nhưng khi sang máy khác thì phải cài IDE này mới build được, việc này hơi bất tiền. Vì thế người ta thường sử dụng Makefile, bởi các lý do sau:

  • Tiện ịch make mặc định có sẵn trên hệ điều hành Ubuntu, Linux.
  • Dễ dàng bảo trì cấu trúc project
  • Dễ dàng chia thành các mô đun theo ý muốn
  • Thời gian build ngắn nếu có sự thay đổi
  • Dễ dàng build trên máy khác nhau và dễ dạng chạy tự động

Nhưng với Makefile, ngoài việc bạn phải biết một số cú pháp cơ bản của Makefile, bạn còn phải biết các lệnh của trình dịch.

Bài này sẽ hướng dẫn chi tiết về Makefile và tiện ích make.

Makefile là gì và cách build từ Makefile?

Makefile là một file dạng script chứa các thông tin cần thiết để build một project:

  • Thông tin project (Tên, version, ...)
  • Thông tin tệp mã nguồn (source code) để build
  • Thông tin trình biên dịch
  • Thông tin các thư viện cần link tới
  • ..

Lệnh make (Tiện ích make) sẽ đọc nội dung Makefile và thực thi các lệnh trong đó. Tiện ích make mặc định có trên máy sử dụng hệ điều hành Ubuntu, Linux, Centos và tích hợp trên công cụ MinGw, Cygwin hỗ trợ build trên Windows.

Để build từ Makefile, bạn bật Terminal, chuyển đến thư mục có chức tệp Makefile, sau đó đánh lệnh sau:

make

Trường hợp này make sẽ tự động tìm tệp Makefile trong thư mục hiện tại để đọc và thực thi. Trường hơp tên không phải là Makefile thì bạn có thể dùng lệnh sau:

make -f <Tên tệp Makefile> <Tên rule>
make --file=<Tên tệp Makefile> <Tên rule>
make --makefile=<Tên tệp Makefile> <Tên rule>

Ví dụ:

make -f standalone.mk
make -file=standalone.mk
make --makefile=standalone.mk

Ngoài ra bạn có thể tạo nhiều Job thực hiện build thông qua tham số -j, việc này sẽ giúp build nhanh hơn. Số lượng các job nên bằng số core của CPU để tận dụng tính toán song song. Ví dụ:
make -j4

Tạo một Makefile đơn giản

Cú pháp Rule

Rule xác định thời điểm và cách thức để tạo ra tệp đích

Một Makefile đơn giản sẽ bao gồm một hoặc nhiều "rule", mỗi rule theo cú pháp sau:

target target ...: dependency dependency ...
        command
        ...
        ...

hoặc:

target target ...: dependency dependency ...; command
        command
        ...

Trong đó:

  • target: Thường là tên một file được sinh ra, ví dụ như tệp thực thi hay các tệp objects. Ngoài ra, nó cũng có thể là tên của một hành động để thực hiện như "clean". Có thể có nhiều target, các target phân chia bởi dấu cách (space).
  • dependency: Là tệp đầu vào để tạo target, nó có thể là tệp đầu vào, hay cũng có thể trỏ tới 1 target khác.
  • command: Là hành động được make thực hiện. Một rule có thể có nhiều hơn 1 lệnh, mỗi lệnh trên một dòng. Chú ý: Bạn cần phải đặt một kí tự "tab" ở đầu mỗi dòng lệnh. Mọi người phải cẩn thận ở điểm này để tránh lỗi không cần thiết.

Tạo Makefile đơn giản

Dưới đây là một Makefile đơn giản để qua đó giải thích cho các bạn cách make xử lý một Makefile.

edit : main.o kbd.o command.o display.o \
       insert.o search.o files.o utils.o
        cc -o edit main.o kbd.o command.o display.o \
                   insert.o search.o files.o utils.o

main.o : main.c defs.h
        cc -c main.c
kbd.o : kbd.c defs.h command.h
        cc -c kbd.c
command.o : command.c defs.h command.h
        cc -c command.c
display.o : display.c defs.h buffer.h
        cc -c display.c
insert.o : insert.c defs.h buffer.h
        cc -c insert.c
search.o : search.c defs.h buffer.h
        cc -c search.c
files.o : files.c defs.h buffer.h command.h
        cc -c files.c
utils.o : utils.c defs.h
        cc -c utils.c
clean :
        rm edit main.o kbd.o command.o display.o \
           insert.o search.o files.o utils.o

Khi bạn đánh lệnh:

make

Make sẽ tìm đọc tệp Makefile trong thư mục hiện tại và bắt đầu xử lý rule đầu tiên, trong ví dụ nay nó chính là rule "edit". Đầu tiên Make duyệt lần lượt các input và xử lý chúng:

  • Đầu tiên gặp input là tệp main.o, make sẽ kiểm tra tệp này có tồn tại không. Nếu tệp không tồn tại, make sẽ tự động tìm rule và xử lý rule "main.o", rule này build tệp main.o từ tệp main.cdefs.h.
  • Make xử lý tương tự cho các input khác.

Sau khi xử lý xong các input cho rule "edit", make sẽ thực hiện lệnh trong rule "edit". Lệnh này sẽ làm nhiệm vụ link các tệp objects *.o thành tệp thực thi edit. Ngoài ra make còn có cơ chế so sánh timestamp giữa các file để biết file nào mới thay đổi thực hiện build lại.

Bây giờ nếu bạn đánh lệnh:

make clean

Make sẽ tìm đọc tệp Makefile trong thư mục hiện tại và xử lý rule "clean". Rule này sẽ thực hiện xóa tất cả các object file.

Một số lệnh cơ bản trong Makefile

Chi tiết về Makefile, bạn xem đầy đủ nhất tại địa chỉ: http://web.mit.edu/gnu/doc/html/make_toc.html

Ở đây tôi xin giới thiệu một số lệnh cơ bản hay sử dụng:

Comments

Môt comment được xác định bởi kí tự "#", tất cả các kí tự sau kí tự "#" đến cuối dòng sẽ bị bỏ qua. Ví dụ:

#
# Makefile for Opus Make 6.1
#
# Compiler: Microsoft C 6.0
# Linker: Microsoft Link 5.10
# 

some makefile statement # a comment 

Trường hợp sử dụng dấu nối dòng "\Enter" thì comment chỉ có tác dụng đến hết dòng đó, dòng sau vẫn thực hiện bình thường. Tức là trường hợp sau:

line_one \
line_two # more_line_two \
line_three 

sẽ tương đương với:

line_one line_two line_three

Continued Lines (Dấu nối dòng)

Trong trường hợp Makefile có dòng rất dài, bạn có thể break nó thành nhiều dòng bằng cách đặt “'\'enter” ở cuối dòng. Ví dụ sau:

first_part_of_line second_part_of_line

Sẽ tương đương với:

first_part_of_line \
second_part_of_line

Dependency Lines

Các dòng có dấu hai chấm ":" được gọi là "dependency lines". Phía bên trái dấu ":" là target, phía bên phải là các đầu vào cần để tạo thành target. Thực chất nó chính là phần input trong rule.

Trong ví dụ dưới:

project.exe : '''main.obj io.obj'''

"project.exe" phụ thuộc vào "main.obj" và "io.obj". Ở thời điểm chạy, make sẽ so sánh thời gian "project.exe" được thay đổi cuối cùng với thời gian thay đổi cuối của "main.obj" và "io.obj", nếu "main.obj" hoặc "io.obj" mới hơn, make sẽ thực hiện build lại "project.exe".

Việc này so sánh này cũng sẽ được thực hiện đệ quy. Ví dụ như dưới:

project.exe : main.obj io.obj 

main.obj : main.c 

Make kiểm tra nếu main.c mới hơn thì main.obj sẽ được build lại. Khi đó main.obj sẽ mới hơn vàproject.exe sẽ được build lại.

Shell Lines

Chính là phần lệnh trong rule, xác định danh sách các lệnh sẽ thực hiện cho rule đó. Trong phần lệnh này, bạn gọi lệnh shell bắt kỳ.

project.exe : main.o kbd.o command.o display.o
    @echo Linking project.exe
    cc -o edit main.o kbd.o command.o display.o

Lệnh đầu tiên hiển thị thông báo "Linking project.exe". Lệnh thứ 2 gọi lệnh cc để link các tệp object *.o thành tệp thực thi project.exe.

Define Variables (Khai báo biến)

Biến giúp bạn giảm lượng text lặp lại trong Makefile. Trong Makefile, bạn có thể định nghĩa một hoặc nhiều biến theo cú pháp sau:

<NAME> = <VALUE>

Ngoài ra bạn có thể định nghĩa theo cú pháp:

<NAME> := <VALUE>

Và trong Makefile, biểu thức dạng $(name) hoặc ${name} sẽ được thay thế bằng giá trị của biến có tên là name. Dưới đây là ví dụ định nghĩa 04 biến ở đầu Makefile:

OBJS = main.obj io.obj
MODEL = s
CC = bcc
CFLAGS = –m$(MODEL) 

project.exe : $(OBJS)
	tlink c0$(MODEL) $(OBJS), project.exe,, c$(MODEL) /Lf:\bc\lib 

main.obj : main.c
	$(CC) $(CFLAGS) –c main.c 

io.obj : io.c
	$(CC) $(CFLAGS) –c io.c 

$(OBJS) : incl.h 

Bạn cũng cần chú ý sự khác nhau khi sử dụng "=" và ":=":

  • "=": Nếu giá trị có biến khác thì nó tham chiếu tới biến đó. Vì thế biến được tham chiếu đến có thể thiết lập sau cũng được.
  • ":=": Nếu giá trị có biến khác thì nó lấy luôn giá trị biến đó cho vào. Vì thế biến tham chiếu nên được định nghĩa trước.

Ví dụ ban có Makefile sau:

surname = Nguyen Van
name = $(surname) A
$(info name=$(name))
 
surname = Dao Trong
$(info name=$(name))

all:
        @echo "Done"

Kết quả là:

name=Nguyen Van A
name=Dao Trong A
Done

Trong khi nếu thay "=" bằng ":=" khi thiết lập biến name như sau:

surname = Nguyen Van
name := $(surname) A
$(info name=$(name))
 
surname = Dao Trong
$(info name=$(name))

all:
        @echo "Done"

Thì kết quả sẽ là:

name=Nguyen Van A
name=Nguyen Van A
Done

Environment Variables (Các biến môi trường)

Một biến môi trường (Được export) có thể được tham chiếu dễ dàng trong Makefile và nó như là một biến trong Makefile. Ví dụ Makefile sau hiển thị biến môi trường PATH của hệ thống:

$(info PATH=$(PATH))
all:
        @echo "Done"

Automatic Variables (Các biến tự động)

Để thuận tiện khi viết Makefile, bạn có thể sử dụng các biến tự động. Dưới đây là bảng liệt kê một số biến tự động:

# Variable Description Example
1 $@ Tên target trong rule. Nếu rule có nhiều target, nó là tên target mà lệnh tác động lên. 

test:
    @echo $@

2 $%

Tên target trong rule khi target là một "archive member". Ví dụ, nếu target là foo.a(bar.o) thì "$%" chính là bar.o, còn "$@" chính là foo.a

foo.a(bar.o):
    @echo $@
    @echo $%
3 $< Tên của dependency bắt buộc đầu tiên. Trường hợp rule ẩn, nó chính là input đầu tiên để tạo target.

 

foo : foo.o bar.o
    @echo $<

foo.o:
    touch foo.o

bar.o:
    touch bar.o

4 $? Tên của tất cả các dependency bắt buộc và mới hơn target. Các dependency ngăn cách nhau bởi dấu cách. Trường hợp "archive member", nó chỉ là tên của member được sử dụng.

foo : foo.o bar.o
    @echo $?

foo.o:
    touch foo.o

bar.o:
    touch bar.o

5 $^ Tên của tất cả các dependency bắt buộc. Các dependency ngăn cách nhau bởi dấu cách. Trường hợp "archive member", nó chỉ là tên của member được sử dụng.

foo : foo.o bar.o
    @echo $^

foo.o:
    touch foo.o

bar.o:
    touch bar.o

6 $+

Giống như $^, nhưng các dependency được sắp xếp theo thứ tự trong Makefile.

 
7 $| Tên của tất cả các dependency bắt buộc, theo đúng thứ tự. Các dependency ngăn cách nhau bởi dấu cách.   
8 $* Là phần nội dụng được match trong một rule ẩn. Ví dụ như trong ví dụ bên cạnh thì $* chính là foo dir/a.foo.b : a.foo.b
    @echo $@
a.%.b:
    @echo $*
    touch $@

Ngoài ra bạn có thể gán thêm hậu tố D (Để lấy phần thư mục mà không có dấu / ở cuối), F (Để lấy phần tên file) vào sau. Ví dụ:

  • $(@D): Lấy phần thư mục trong target. 
  • $(@F): Lấy phần file name trong target.
  • Tương tự cho $(*D), $(*F), $(%D), $(%F), ...

Modifiers

Modifier là quy tắc giúp thay đổi giá trị của một biến theo quy luật nào đó. Cú pháp tổng quát như sau:

$(name:modifier[,modifier ...])

Trong đó:

  • name: Là tên biến muốn thay đổi.
  • modifier: Là quy tắc thực hiện thay đổi.

Dưới đây là một số modifer hay sử dụng:

# Modifier Description Example
1 from=to Thay thế xâu from thành xâu to. Nếu bạn để xâu bình thường thì nó chỉ tìm và thay thế ở cuối mỗi từ. Nếu bạn muốn thay thế ở đầu, bạn phải sử dụng thêm kí tự đặc biệt % ở cuối trong xâu tìm kiếm và xâu thay thế. Trường hợp khác bạn nên sử dụng hàm thao tác xâu.

OBJS = main.obj io.obj
SRCS = $(OBJS:.obj=.c)

=> SRCS = main.c io.c

OBJS = mda_main.obj mda_io.obj
NEW_OBJS = $(OBJS:mda_%=sup_%)

=> NEW_OBJS = main.c io.c

Inference Rules (Các rule suy luận)

Các rule suy luận sẽ được xử lý khi thực thi make, vì vậy bạn không cần phải đưa ra rule rõ ràng cho mỗi target. Trong trường hợp này, bạn sử dụng kí tự đặc biệt %, kí tự này match với 0 hoặc một tập các kí tự. Dưới đây là rule ví dụ để compile tệp mã nguồn C (.c) thành tệp đối tượng (.o):

%.obj : %.c
	$(CC) $(CFLAGS) –c $(.SOURCE)

Makefile Directives (Lệnh điều kiện trong Makefile)

Ví dụ Makefile dưới sử dụng %if, %elif, %else%endif để hỗ trợ cả trình dịch Borland và Microsoft:

# This makefile compiles the project listed in the PROJ macro
#
PROJ = project			# the name of the project
OBJS = main.obj io.obj		# list of object files 
# Configuration:
#
MODEL = s			# memory model
CC = bcc			# name of compiler 
# Compiler-dependent section
#
%if $(CC) == bcc		# if compiler is bcc
  CFLAGS = –m$(MODEL)		# $(CFLAGS) is –ms
  LDSTART = c0$(MODEL)		# the start-up object file
  LDLIBS = c$(MODEL)		# the library
  LDFLAGS = /Lf:\bc\lib		# f:\bc\lib is library directory
%elif $(CC) == cl		# else if compiler is cl
  CFLAGS = –A$(MODEL,UC)	# $(CFLAGS) is –AS
  LDSTART =			# no special start-up
  LDLIBS =			# no special library
  LDFLAGS = /Lf:\c6\lib;	# f:\c6\lib is library directory
%else				# else
% abort Unsupported CC==$(CC)	# compiler is not supported
%endif				# endif 
# The project to be built
#
$(PROJ).exe : $(OBJS)
	tlink $(LDSTART) $(OBJS), $(.TARGET),, $(LDLIBS) $(LDFLAGS) 
$(OBJS) : incl.h

Using Wildcard in File Names (Sử dụng kí tự Wildcards trong tên file)

Tên một file đơn có thể xác định nhiều file nếu bạn sử dụng các kí tự Wildcard. Các kí tự wildcard sử dụng trong make bao gồm: *, ?, [...]. Ví dụ: *.c: Danh sách các file có tên file kết thúc bằng .c.

Ngoài ra kí tự ~ ở đầu tên tệp cũng có ý nghĩa đặc biệt:

  • Nếu đứng một mình hoặc theo sau bởi kí tự /, thì nó xác định "home directory" của user hiện tại. Ví dụ "~/bin", nếu bạn đang đăng nhập vào account demo trên Ubuntu, thì nó chính là "/home/demo/bin'.
  • Nếu kí tự ~ theo sau bởi 1 từ, thì nó xác định "home directory" của user chính là từ từ theo sau. Ví dụ: "~john/bin" thì giá trị của nó chính là "/home/john/bin'.

Nếu bạn muốn make không xử lý kí tự đặc biệt này, bạn hãy thêm kí tự \ vào đằng trước. Ví dụ "foo\*bar" thì nó bao gồm chữ foo, dấu * và sau đó là chữ bar.

Sau đây là ví dụ sử dụng wildcard trong phần Shell Lines để xóa tất cả các tệp đối tượng:

clean:
		rm -f *.o

Một ví dụ khác sử dụng Wildcard trong dependency:

print: *.c
		lpr -p $?
		touch print

Nhưng bạn cần chú ý, biểu thức Wildcard không có tác dụng khi bạn định nghĩa 1 biến, hay trong giá trị của một tham số, tức là không tự động mở rộng giá trị. Trường hợp nếu muốn bạn phải sử dụng hàm wildcard. Như trường hợp ở dưới thì giá trị thực sự của objects là "*.o". Vì thế khi sử dụng phải hết sức cẩn thận.

objects = *.o

foo : $(objects)
        cc -o foo $(CFLAGS) $(objects)

Searching Directories for Dependencies (Thư mục tìm kiếm các Dependency)

Thường với các hệ thống lớn, thư mục mã nguồn được đặt trong nhiều thư mục. Make hỗ trợ tính năng cho phép tìm kiếm trong nhiều thư mục, trong trường hợp bạn có đổi thư mục của mã nguồn, bạn cũng không cần phải thay đổi rule.

Thông thường make tìm kiếm file trong thư mục hiện tại, nhưng hợp bạn muốn make tìm ở nhiều thư mục hãy thiết lập giá trị cho biến VPATH. Ví dụ:

VPATH = src:../headers

Xác định path có hai thư mục"src" và "../headers", make sử tự động tìm trong các thư mục này theo thứ tự.

Ngoài ra bạn cũng có thể sử dụng chỉ thị vpath (Viết thường), cho phép bạn xác định một search path đến từng file cụ thể. Có ba dạng sử dụng vpath như sau:

  • vpath pattern directories: Chỉ định thư mục tìm kiếm cho tên tệp khớp với pattern đưa ra. directories, là một danh sách các thư mục sử dụng để tìm kiếm, phân cách nhau bởi dấu hai chấm hoặc kí tự trắng. Trong pattern, sử dụng kí tự% như "%.h" match với các tệp có tận cùng là ".h". Sử dụng \% để make hiểu đó là kí tự % đơn thuần.
  • vpath pattern: Xóa đường dẫn tìm kiếm đi kèm với mẫu.
  • vpath: Xóa tất cả các đường dân tìm kiếm được chỉ định với các chỉ thị vpath trước đó.

Ví dụ:

vpath %.c foo:bar
vpath %   blish

Including Other Makefiles

Chỉ thị include bảo make đọc tệp makefile khác trước khi tiếp tục. Cú pháp như sau:

include filenames...

Tên file có thể chứa pattern, có thể sử dụng biến. Tên file phân cách bởi dấu space, không được sử dụng kí tự tab.

Ví dụ nếu có ba tệp .mk là a.mk, b.mkc.mk và biến $(bar) bằng "bish bash" thì:

include foo *.mk $(bar)

tương đương với:

include foo a.mk b.mk c.mk bish bash

File trong include được tìm trong thư mục hiện tại, nếu không có sẽ tìm trong "searched directories".

Nếu muốn make bỏ qua một số file sử dụng lệnh sau trong Makefile:

-include filenames...

Conditional Parts of Makefiles (Điều kiện trong Makefile)

Đôi khi trong Makefile, bạn cần kiểm tra giá trị một biến để xác định dòng nào tiếp theo trong Makefile được thực hiện, trường hợp này bạn sử dụng lệnh điều kiện. Cú pháp lệnh như sau (T/Hợp không có else):

conditional-directive
text-if-true
endif

Trong trường hợp có else, cú pháp đầy đủ như sau:

conditional-directive
text-if-true
else
text-if-false
endif

Bảng sau liệt kê một số chỉ thị điều kiện (conditional-directive) sử dụng:

# Chỉ thị điều kiện Mô tả Ví dụ
1 ifeq (arg1, arg2)
ifeq 'arg1' 'arg2'
ifeq "arg1" "arg2"
ifeq "arg1" 'arg2'
ifeq 'arg1' "arg2"
Chỉ thị so sánh bằng trong đó arg1, arg2 có thể là các xâu hoặc tham chiếu tới các biến. Nên sử dụng hàm strip để chuẩn hóa giá trị các biến trước khi so sánh. ifeq ($(strip $(foo)),)
$(info "Text is empty")
endif
2 ifneq (arg1, arg2)
ifneq 'arg1' 'arg2'
ifneq "arg1" "arg2"
ifneq "arg1" 'arg2'
ifneq 'arg1' "arg2"
Chỉ thị so sánh không bằng trong đó arg1, arg2 có thê là các xâu hoặc tham chiếu tới các biến. foo = main.c
ifneq ($(strip $(foo)),)
$(info "Text is not empty")
endif
3 ifdef variable-name Chi thị kiểm tra một biến đã được định nghĩa hay chưa, nếu đã định nghĩa sẽ trả về true. Các biến chưa được định nghĩa thì sẽ có giá trị rỗng. Chú ý rằng ifdef trả về true cho tất cả các định nghĩa trừ định nghĩa dạng như foo = .

Trường hợp dưới frobozz được thiết lập bằng "yes":

bar =
foo = $(bar)
ifdef foo
frobozz = yes
else
frobozz = no
endif
$(info "frobozz = ${frobozz}")

Còn trường hợp sau frobozz được thiet lập bằng "no":

foo =
ifdef foo
frobozz = yes
else
frobozz = no
endif
$(info "frobozz = ${frobozz}")

4 ifndef variable-name Chỉ thị kiểm tra một biến có phải chưa được định nghĩa hay không?  

Phony Targets (Các target giả)

"Phony target" thực sự nó không phải là tên của một tệp, nó chỉ là tên để thực hiện một số lệnh khi bạn cần yêu cầu rõ ràng. Có hai lý do sử dụng phony target (Mục tiêu giả mạo):

  • Để tránh xung đột với một file có cùng tên.
  • Để tăng hiệu năng xử lý

Ví dụ bạn viết một rule, nhưng các lệnh trong rule không tạo ra tệp target:

clean:
        rm -rf *.obj temp

Lệnh rm không tạo tệp "clean", và có lẽ tệp này không tồn tại, vì thế mỗi này bạn đánh "make clean" thì lệnh rm sẽ được thực hiện. Nhưng trường hợp có tệp "clean" thì lệnh rm sẽ không được thực hiện. Để tránh trường hợp này ta thêm một lệnh để đánh dấu "clean" là một mục tiêu giả (phony target):

.PHONY: clean
clean:
        rm -rf *.obj temp

Trường hợp này lệnh rm luôn được thực hiện khi bạn đánh "make clean", không phụ thuộc vào tệp "clean" có tồn tại hay không.

Phony target có thể có phụ thuộc. Ví dụ:

all : prog1 prog2 prog3
.PHONY : all

prog1 : prog1.o utils.o
        cc -o prog1 prog1.o utils.o

prog2 : prog2.o
        cc -o prog2 prog2.o

prog3 : prog3.o sort.o utils.o
        cc -o prog3 prog3.o sort.o utils.o

Một Phony Target phụ thuộc vào target khác, nó như một chương trình con. Ví dụ:

.PHONY: cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff
        rm program

cleanobj :
        rm *.o

cleandiff :
        rm *.diff

Đánh lệnh "make cleanall" sẽ xóa tất cả các tệp "*.o", "*.diff" và "program".

Special Built-in Target Names (Một số tên target đặc biệt)

Bảng sau liệt kê một số tên target với ý nghĩa đặc biệt:

# Tên target Mô tả
1 .PHONY Một target giả. Khi được yêu cầu thực hiện target này, make sẽ thực hiện tất cả các lệnh của nó một cách vô điều kiện, dù file target đó có tồn tại hay không, dù thời gian thay đổi cuối cùng của nó có như thế nào đi nữa.
2 .SUFFIXES .SUFFIXES là danh sách các hậu tố được sử dụng để kiểm tra các quy tắc hậu tố.
3 .DEFAULT Trong trường hợp lệnh make không chỉ cụ thể target nào thì target này sẽ được thực hiện. Trường hợp .DEFAULT không được định nghĩa thì rule đầu tiên sẽ được thực hiện.
4 .PRECIOUS

Là target đặc biệt. Khi make bị kill hoặc bị ngắt thì target này không bị xóa.

5 .IGNORE

Các dependency cho .IGNORE sẽ được bỏ qua các lỗi khi thực hiện lệnh.

6 .SILENT

Các dependency cho .SILENT, make sẽ không hiển thị các lệnh tạo lại tệp xác định trước khi thực hiện.

7 .EXPORT_ALL_VARIABLES

Coi như target đặc biệt, nó bảo make thực hiện xuất tất cả các biến để sử dụng cho process con.

Communicating Variables to a Sub-make (Sử dụng biến trong sub-make)

Trong một Makefile, phần command của rule, bạn gọi lệnh make, thì Makefile được sử dụng trong lệnh make này gọi là Sub-make. Thông thường, khi khai báo biến trong Makefile, bạn chỉ sử dụng biến trong Makefile đó, không sử dụng được trong các Sub-make. Để sử dụng được biến trong Sub-make con, bạn phải sử dụng chỉ thị export. Cú pháp như sau:

export variable ...

Trường hợp muốn ngăn cản biến đã được export sử dụng Makefile con bạn sử dụng:

unexport variable ...

Để thuận tiện hơn, bạn có thể vừa thiết lập giá trị vừa thực hiện export. Ví dụ:

export variable = value

giống như lệnh:

variable = value
export variable

Và trường hợp:

export variable := value

có kết quả giống như:

variable := value
export variable

Hoặc:

export variable += value

giống như:

variable += value
export variable

Communicating Options to a Sub-make (Truyền option cho Sub-make)

Khi gọi Sub-make, bạn truyền thông số cho make là được. Ví dụ:

subsystem:
        cd subdir; $(MAKE) $(MFLAGS)

Functions in Makefile (Sử dụng hàm trong Makefile)

Trong Makefile, cú pháp gọi hàm như sau:

$(function arguments)

hoặc:

${function arguments}

Trong đó:

  • function: Tên hàm mà make hỗ trợ
  • arguments: Là danh sách các tham số của hàm. Tham số phân cách với tên hàm bởi một hoặc nhiều kí tự space hoặc tab. Nếu có nhiều tham số, các tham số cách nhau bởi dấu phẩy ",". Nếu đối số chứa lệnh khác hoặc tham chiếu tới một biến thì nên sử dụng thống nhất một loại phân tách, ví dụ nên viết "$(subst a,b,$(x))", chứ không nên viết "$(subst a,b,${x})".

Dưới đây là một sô hàm hay sử dụng:

# Function Description
1 $(wildcard pattern...)

Hàm này được sử dụng ở bất kỳ đâu trong Makefile. Hàm này trả về danh sách các tệp match với một trong các pattern được đưa ra. Nếu có nhiều pattern, các pattern ngăn cách bởi kí tự space.

Ví dụ bạn muốn liên kê hết tên file tận cùng là .o:

$(wildcard *.o)

Trường hợp bạn muốn liệt kê hết các tệp tận cùng là .c hoặc .cpp:

$(wildcard *.cpp *.c)

 

2 $(subst from,to,text)
Hàm tìm kiếm xâu from trong text và thay thế bằng xâu to. Ví dụ:
$(subst ee,EE,feet on the street) = "fEEt on the strEEt".
3 $(patsubst pattern,replacement,text)

Tìm các từ được phân cách bằng khoảng trắng trong văn bản khớp với mẫu và thay thế chúng bằng xâu replacement. Pattern chứa kí tự wildcard "%". Khoảng trắng giữa các từ được gộp lại thành ký tự trắng duy nhất, khoảng trống đầu và cuối dấu bỏ.

Ví dụ:

$(patsubst %.c,%.o,x.c.c bar.c) = "x.c.o bar.o"

Ví dụ khác:

objects = foo.o bar.o baz.o
$(patsubst %.o,%.c,$(objects))

Chú ý rằng:

  • $(var:pattern=replacement)  tương đương với $(patsubst pattern,replacement,$(var))
  • $(var:suffix=replacement) tương đương với $(patsubst %suffix,%replacement,$(var))
4 $(strip string)

Hàm này loại bỏ ký tự trắng ở đầu và cuối xâu, đồng thời thay thế nhiều kí tự trắng liên tiếp thành kí tự trắng duy nhất.. Ví dụ: $(strip a   b   c ) = "a b c"

Hàm này rất hữu ích nên sử dụng trước khi gọi các lệnh so sánh để tránh kết quả không mong muốn. Ví dụ:

.PHONY: all
ifneq   "$(needs_made)" ""
all: $(needs_made)
else
all:;@echo 'Nothing to make!'
endif

Nếu thực hiện thay thế $(needs_made) bằng $(strip $(needs_made)) thì sẽ chuẩn hơn nhiều.

5 $(findstring find,in)

Hàm này tìm xâu find xuất hiện trong xâu in, nếu có trả về xâu tìm thấy, không tìm thấy trả về xâu rỗng. Ví dụ:

$(findstring a,a b c) = "a"
$(findstring a,b c) = ""

6 $(filter pattern...,text)

Loại bỏ tất cả các từ (các từ phân cách bởi dấu cách) không match với pattern, trả về chỉ các từ match với pattern. Xâu patter sử dụng kí tự "%". Ví dụ:

$(info $(filter %.c %.s,foo.c bar.c baz.s ugh.h)) = "foo.c bar.c baz.s"

7 $(filter-out pattern...,text)

Loại bỏ các từ match với pattern, trả về chỉ các từ không match với pattern. Ví dụ:

$(filter-out main1.o main2.o,main1.o foo.o main2.o bar.o) = "foo.o bar.o"

8 $(sort list)

Sắp xếp các từ theo thứ tự và đồng thời loại bỏ các từ trùng lặp. Ví dụ:

$(sort foo bar lose bar) = "bar foo lose"

9 $(dir names...) Hàm lấy phần thư mục trong tên tệp.
$(dir src/foo.c hacks) = "src/ ./"
10 $(notdir names...)

Chiết xuất tất cả trừ phần thư mục của mỗi tên tệp trong tên. Nếu tên tệp không chứa dấu gạch chéo, nó sẽ không thay đổi. Ví dụ:

$(notdir src/foo.c hacks) = "foo.c hacks"

11 $(suffix names...)

Lấy phần hậu tố của mỗi tên tập tin trong tên. Nếu tên tệp chứa một khoảng thời gian, hậu tố là tất cả mọi thứ bắt đầu với khoảng thời gian cuối. Ngược lại thì hậu tố là xâu rỗng. Ví dụ

$(suffix src/foo.c hacks) = ".c"

12 $(basename names...) Triết xuất tất cả trừ phần hậu tố trong tên. Ví dụ:
$(basename src/foo.c hacks) = "src/foo hacks"
13 $(addsuffix suffix,names...) Thêm suffix vào cuối mỗi từ trong tên tệp. Ví dụ:
$(addsuffix .c,foo bar) = "foo.c bar.c"
14 $(addprefix prefix,names...) Thêm prefix vào đầu mỗi từ trong tệp. Ví dụ:
$(addprefix src/,foo bar) = "src/foo src/bar"
15 $(join list1,list2)

Nối hai tham số theo thứ tự từng từ một: nối hai từ đầu tiên trong hai tham số, nối hai từ tiếp theo trong hai tham số,... Nếu một tham số có nhiều từ hơn so với tham số còn lại thì từ đó sẽ không thay đổi trong kết quả. Ví dụ:

$(join a b,.c .o) = "a.c b.o"

16 $(word n,text) Trả về từ thứ n trong text. Số thứ tự tính từ 1. Nếu n lớn hơn số từ có trong text, xâu rỗng được trả về. Ví dụ:
$(word 2, foo bar baz) = "bar"
17 $(words text)

Trả về số từ trong text. Nếu muốn lấy từ cuối cùng trong text sử dụng lệnh sau:

$(word $(words text),text)

18 $(firstword names...)

Trả về từ đầu tiên trong tên. Ví dụ:

$(firstword foo bar) = "foo"

19 $(foreach var,list,text)

Duyệt từng phần tử trong list, mỗi phần từ khi duyệt lưu ở biến var, còn text lệnh thực hiện.

Ví dụ: $(foreach dir,a b c d,$(wildcard $(dir)/*)) tương đương với $(wildcard a/* b/* c/* d/*)

 

20 $(origin variable)

Hàm này trả về thông tin của biến. Nhận các giá trị sau:

  • undefined: Nếu biến chưa được định nghĩa.
  • default: Nếu biến được định nghĩa giá trị mặc đinh. Ví dụ như bien CC,...
  • environment: Nếu biến được định nghĩa như một biến môi trường và lựa chọn "-e" không được bật.
  • environment override: Nếu biến được định nghĩa như biến môi trường và lựa chọn "-e" được bật.
  • file: Nếu biến được định nghĩa trong một Makefile.
  • command line: Nếu biến được định nghĩa bằng dòng lệnh (command line).
  • override: Nếu biến được định nghĩa với một "override directive" trong một Makefile.
  • automatic: Nếu biến là một biến tự động được định nghĩa khi thực hiện lệnh cho từng rule.
21 $(shell command)

Yêu cầu make thực hiện lệnh shell. Ví dụ:

contents := $(shell cat foo)
files := $(shell echo *.c)

22 $(error text…) Sinh ra lỗi với thông điệp hiển thị là text. Khi gặp lệnh này thông báo lỗi được hiển thị và make sẽ bị dừng lại, không thực hiện tiếp.
23 $(warning text…)

Hiển thị thông báo lỗi tương tự như $(error ...) nhưng make vẫn chạy tiếp

24 $(info text…) Hàm này đơn giản là hiển thị nội dung text ra màn hình

Một số ví dụ Makefile build mã nguồn C/C++/Java

Makefile đơn giản build ứng dụng C/C++

Dưới đây là diễn giải từng bước để đi tới một Makefile đơn giản và đầy đủ để build mã nguồn C/C++: File:Hellomake.zip

Ví dụ có ba tệp như sau:

Tệp hellomake.h:

/*
example include file
*/

void myPrintHelloMake(void);

Tệp hellofunc.c:

#include <stdio.h>
#include <hellomake.h>

void myPrintHelloMake(void) {
  printf("Hello makefiles!\n");
}

Tệp hellomake.c:

#include <hellomake.h>

int main() {
  // call a function in another file
  myPrintHelloMake();

  return(0);
}

Để build bạn có thể dùng trực tiếp lệnh của gcc như sau:

gcc -o hellomake hellomake.c hellofunc.c -I.

Nhưng nếu bạn chuyển sang máy khác để build thì bạn phải nhớ và gõ lại toàn bộ câu lệnh. Mặt khác nếu bạn sửa một file .c bạn phải build lại toàn bộ. Để giải quyết hai vấn đề này, bạn sử dụng Makefile đơn giản như sau:

hellomake: hellomake.c hellofunc.c
     gcc -o hellomake hellomake.c hellofunc.c -I.

Makefile này sẽ giúp bạn xử lý vấn đề 1, khi chuyển sang máy khác bạn không cần nhớ lệnh mà chỉ cần đánh make là xong. Nhưng Makefile này vẫn chưa tối ưu cho vấn đề thứ 2, khi 1 file thay đổi vẫn phải build lại tất cả các file *.c. Sửa lại Makefile như sau để giải quyết vấn đề thứ 2:

CC=gcc
CFLAGS=-I.

hellomake: hellomake.o hellofunc.o
    $(CC) -o hellomake hellomake.o hellofunc.o -I.

Trong Makefile trên chúng tên định nghĩa thêm hai biến: CC (Trình dịch sử dụng để build) và CFLAGS (Cờ sử dụng khi dịch). Nhưng Makefile này còn điểm hạn chế, nếu bạn thay đổi tệp hellomake.h, make sẽ không thực hiện build lại các tệp*.c. Để sửa vấn đề này, bạn thay đổi Makefile như sau:

CC=gcc
CFLAGS=-I.
DEPS = hellomake.h

%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

hellomake: hellomake.o hellofunc.o 
	gcc -o hellomake hellomake.o hellofunc.o -I.

Trong Makefile này thêm biến DEPS chứa danh sách các file header *.h sử dụng, sau đó định nghĩa rule áp dụng cho tất cả các file hậu tố là .o phụ thuộc vào file *.c*.h. Ở đây cũng dùng biến đặc biệt $@ để thay thế cho tên tệp trước dấu :$< để thay thế cho tên đầu tiên trong danh sách dependency sau dấu :.

Bây giờ ta tối ưu thêm bằng cách tạo biến OBJ lưu tên các tệp *.o và sử dụng triệt để hơn biến đặc biệt$@$<:

CC=gcc
CFLAGS=-I.
DEPS = hellomake.h
OBJ = hellomake.o hellofunc.o 

%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
	gcc -o $@ $^ $(CFLAGS)

Bây giờ, giả sử ứng dụng bạn sử dụng thư viện toán học -lm. Trường hợp này bạn phải sửa Makefile như dưới. Nói chung đây là bản Makefile tương đối chuẩn nên sử dụng cho các ứng dụng của bạn:

CC = gcc
CFLAGS = -I.
DEPS = hellomake.h
OBJ = hellomake.o hellofunc.o 
LIBS = -lm

%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
	$(CC) -o $@ $^ $(CFLAGS) $(LIBS)

.PHONY: clean

clean:
	rm -f *.o
	rm -f hellomake