Xây dựng chương trình bằng GNU Make. Sử dụng GNU Make hiệu quả

make là một tiện ích để tự động xây dựng chương trình. Cho phép bạn theo dõi các thay đổi trong mã nguồn của chương trình và không biên dịch toàn bộ dự án mà chỉ những tệp đã thay đổi hoặc những tệp phụ thuộc vào những thay đổi được thực hiện. Đối với các dự án lớn, điều này giúp tiết kiệm thời gian đáng kể.

Trong bài viết này tôi sẽ cố gắng hướng dẫn bạn cách tạo một makefile.

Theo mặc định, quy tắc xây dựng được đọc từ tệp có tên Makefile.

Cấu trúc Makefile có thể được biểu diễn như sau:

MỤC TIÊU: HÀNH ĐỘNG PHỤ THUỘC

Nhưng các quy tắc phức tạp hơn thường được sử dụng, ví dụ:

MỤC TIÊU: MỤC TIÊU1 MỤC TIÊU2 HÀNH ĐỘNG MỤC TIÊU1: PHỤ THUỘC1 HÀNH ĐỘNG1 MỤC TIÊU2: PHỤ THUỘC2 HÀNH ĐỘNG2

MỤC TIÊU là kết quả của HÀNH ĐỘNG. Đây có thể là một tập tin, thư mục hoặc đơn giản là một TARGET trừu tượng không có kết nối với bất kỳ đối tượng nào trên ổ cứng. Dấu hai chấm được đặt sau tên mục tiêu. Khi bạn chạy lệnh make không có tham số, quy tắc đầu tiên được tìm thấy sẽ được thực thi. Để thực thi một quy tắc khác, bạn cần chỉ định nó cho lệnh make

Thực hiện MỤC TIÊU2

SỰ PHỤ THUỘC là điều mà MỤC TIÊU của chúng ta phụ thuộc vào. Đây có thể là các tập tin, thư mục hoặc MỤC TIÊU khác. Hãy so sánh ngày giờ thay đổi MỤC TIÊU và đối tượng mà mục tiêu phụ thuộc. Nếu các đối tượng mà mục tiêu phụ thuộc vào được thay đổi muộn hơn so với thời điểm mục tiêu được tạo thì ACTION sẽ được thực thi. ACTION cũng được thực hiện nếu TARGET không phải là tên tệp hoặc thư mục.

MỘT HÀNH ĐỘNG là một tập hợp các lệnh phải được thực thi. Các lệnh phải được bắt đầu bằng ký tự tab. Nếu bạn nhập dấu cách thay vì ký tự tab, thông báo lỗi sẽ hiển thị trong quá trình biên dịch:

Makefile:13: Thiếu dấu phân cách ***. Dừng lại.

Makefile:13: *** thiếu dấu phân cách. Dừng lại.

Ví dụ về tệp thực hiện:

tất cả: test.elf test.elf: test1.o test2.o gcc -o test.elf test1.o test2.o test1.o test1.c gcc -c test1.c -o test1.o test2.o test2.c gcc -c test2.c -o test2.o

Hãy xem xét ví dụ cuối cùng:
Tất cả được thực hiện đầu tiên bởi vì nằm ở phần đầu của Makefile. tất cả đều phụ thuộc vào test.elf và không có tệp hoặc thư mục nào có tên là all, vì vậy nó sẽ luôn kiểm tra mục tiêu có tên test.elf.

test.elf phụ thuộc vào test1.o và test2.o, vì vậy test1.o đích sẽ được kiểm tra trước sau đó là test2.o

Khi kiểm tra mục tiêu test1.o, ngày và giờ sửa đổi của tệp test1.o và test1.c sẽ được so sánh. Nếu tệp test1.o không tồn tại hoặc tệp test1.c được sửa đổi muộn hơn test1.o thì lệnh gcc -c test1.c -o test1.o sẽ được thực thi.

Mục tiêu test2.o sẽ được kiểm tra theo cách tương tự.

Sau đó, ngày và giờ sửa đổi tệp test.elf và tệp test1.o test2.o sẽ được so sánh. Nếu test1.o hoặc test2.o mới hơn thì lệnh gcc -o test.elf test1.o test2.o sẽ được thực thi

Bằng cách này, những thay đổi trong tệp test1.c và test2.c sẽ được theo dõi.

P/S Tôi hy vọng ghi chú này sẽ đơn giản hóa việc tạo tệp makefile, nhưng nếu bạn có bất kỳ câu hỏi nào, hãy viết bình luận, tôi sẽ cố gắng trả lời.

Thông thường, việc xây dựng một dự án trên hệ điều hành Linux, có tính đến các phần phụ thuộc và cập nhật, được thực hiện bởi tiện ích làm, sử dụng tập lệnh xây dựng được định dạng sẵn cho việc này. Chúng tôi đã nhiều lần nhờ đến sự trợ giúp của tiện ích này trong các bài viết trước và bài viết này sẽ dành riêng cho các vấn đề sử dụng tiện ích này làm.

làm tiện ích

Tính thiết thực làm tự động xác định phần nào của một dự án lớn đã thay đổi và cần được biên dịch lại, đồng thời thực hiện các hành động cần thiết để thực hiện việc đó. Tuy nhiên, trên thực tế, phạm vi tạo không chỉ giới hạn ở việc xây dựng chương trình, nó còn có thể được sử dụng để giải quyết các vấn đề khác trong đó một số tệp sẽ được cập nhật tự động khi các tệp khác bị thay đổi.

Tính thiết thực làm có sẵn cho các hệ điều hành khác nhau và do các tính năng triển khai, cùng với việc triển khai “bản địa”, nhiều hệ điều hành có triển khai GNU gmake và hoạt động của những triển khai này trong một số hệ điều hành, ví dụ như Solaris, có thể khác nhau đáng kể. Do đó, nên chỉ định tên của một tiện ích cụ thể trong tập lệnh xây dựng. Trong hệ điều hành Linux, hai tên này là từ đồng nghĩa, được triển khai thông qua một liên kết tượng trưng, ​​​​như dưới đây:

$ ls -l /usr/bin/*make lrwxrwxrwx 1 root root 4 ngày 28 tháng 10 năm 2008 /usr/bin/gmake -> make -rwxr-xr-x 1 root root 162652 25 tháng 5 năm 2008 /usr/bin/make ... $ tạo --version GNU Make 3,81 ...

Theo mặc định, tên tệp tập lệnh xây dựng là Makefile. Tính thiết thực làm cung cấp lắp ráp hoàn chỉnh của quy định bàn thắng có trong kịch bản, ví dụ:

$ làm $ làm sạch

Nếu mục tiêu không được xác định rõ ràng thì tuần tự đầu tiên target trong tập tin script. Bạn cũng có thể chỉ định bất kỳ tệp tập lệnh nào khác sẽ được sử dụng để hợp ngữ:

$ make -f Makefile.my

Tập tin đơn giản nhất Makefile bao gồm các cấu trúc cú pháp của hai loại: mục tiêu và định nghĩa vĩ mô. Mô tả mục tiêu bao gồm ba phần: tên mục tiêu, danh sách các phần phụ thuộc và danh sách các lệnh shell cần thiết để xây dựng mục tiêu. Tên mục tiêu là một danh sách không trống các tệp được cho là sẽ được tạo. Danh sách phụ thuộc - danh sách các tệp tùy thuộc vào mục tiêu được xây dựng. Tên mục tiêu và danh sách các phần phụ thuộc tạo nên tiêu đề đích, được viết trên một dòng và phân tách bằng dấu hai chấm (: //). Danh sách các lệnh được viết từ dòng tiếp theo, với tất cả các lệnh bắt đầu với ký tự tab được yêu cầu. Nhiều trình soạn thảo văn bản có thể được cấu hình để thay thế các ký tự tab bằng dấu cách. Thực tế này đáng được tính đến và kiểm tra xem trình soạn thảo mà bạn đang chỉnh sửa có Makefile, không thay thế các tab bằng dấu cách, vì sự cố này xảy ra khá thường xuyên. Bất kỳ dòng nào trong chuỗi danh sách lệnh không bắt đầu bằng tab (lệnh khác) hoặc " # " (bình luận) - được coi là việc hoàn thành mục tiêu hiện tại và bắt đầu một mục tiêu mới.

Tính thiết thực làm có nhiều tham số bên trong với các giá trị mặc định, trong đó quan trọng nhất là các quy tắc xử lý hậu tố, cũng như định nghĩa về các biến môi trường bên trong. Dữ liệu này được gọi là cơ sở dữ liệu làm và có thể được xem như thế này:

$ make -p >make.suffix make: *** Không có mục tiêu nào được chỉ định và không tìm thấy tệp makefile. Dừng lại. $ cat make.suffix # GNU Make 3.81 # Copyright (C) 2006 Free Software Foundation, Inc. ... # Tạo cơ sở dữ liệu, in Thứ năm 14 tháng 4 14:48:51 2011 ... CC = cc LD = ld AR = ar CXX = g++ COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $ ( TARGET_ARCH) -c COMPILE.C = $(COMPILE.cc) ... SUFFIXES:= .out .a .ln .o .c .cc .C .cpp .p .f .F .r .y .l . s .S .mod .sym \ .def .h .info .dvi .tex .texinfo .texi .txinfo .w .ch... # Quy tắc ngầm định ... %.o: %.c # các lệnh sẽ được thực thi (được xây dựng -in): $(COMPILE.c) $(OUTPUT_OPTION) $< ...

Giá trị của tất cả các biến này là: CC, LD, AR, EXTRA_CFLAGS, ... có thể được tệp tập lệnh sử dụng làm định nghĩa ngầm định với các giá trị mặc định. Ngoài ra, bạn có thể tự xác định quy tắc xử lý mặc định cho lựa chọn hậu tố(phần mở rộng tên tệp), như trong ví dụ trên đối với tệp nguồn mã C: %.c.

Hầu hết các môi trường phát triển tích hợp (IDE) hoặc gói để tạo cài đặt di động (ví dụ: automake hoặc autoconf) đều có nhiệm vụ tạo tệp Makefile cho tiện ích làm.

Cách tăng tốc độ xây dựng

Việc lắp ráp các dự án đơn giản diễn ra khá nhanh chóng, nhưng có tính đến sự phát triển của dự án khi nó phát triển, thời gian lắp ráp, phần chính dành cho việc biên soạn, có thể tăng lên đáng kể. Một ví dụ nổi tiếng về loại này là việc xây dựng nhân Linux, tùy thuộc vào loại phần cứng, có thể mất từ ​​​​hàng chục phút đến hàng giờ CPU. Tình hình trở nên trầm trọng hơn khi thực hiện một dự án (sửa đổi mã, gỡ lỗi, tìm lỗi, kiểm tra, v.v.), bạn có thể cần phải xây dựng lại dự án vài chục lần một ngày. Do đó, các cơ hội để tăng tốc quá trình này trở nên thực sự phù hợp.

Do các hệ thống bộ xử lý đơn (lõi đơn) ngày nay hầu như đã được thay thế bằng cấu hình đa lõi, nên việc lắp ráp nhiều dự án có thể được tăng tốc đáng kể (đa dạng) bằng cách tận dụng cơ hội này. làm chạy song song nhiều công việc xây dựng bằng phím –j như sau:

$ man make ... -j , --jobs[=jobs] Chỉ định số lượng công việc (lệnh) sẽ chạy đồng thời. ...

Hãy kiểm tra những lợi ích mà tính năng này mang lại bằng một ví dụ thực tế. Để làm tiêu chuẩn cho việc lắp ráp, hãy lấy một dự án máy chủ NTP, dự án này không mất nhiều thời gian để lắp ráp nhưng cũng không quá nhanh:

$ pwd /usr/src/ntp-4.2.6p3

Trước tiên, hãy chạy bản dựng trên bộ xử lý Atom 4 nhân (không phải model quá nhanh với tần số 1,66GHz) nhưng với ổ SSD rất nhanh:

$ cat /proc/cpuinfo | head -n10 bộ xử lý: 0 nhà cung cấp_id: Chính hãngIntel cpu family: 6 model: 28 tên model: Intel(R) Atom(TM) CPU 330 @ 1.60GHz step: 2 cpu MHz: 1596.331 kích thước bộ đệm: 512 KB $ làm sạch # khởi động xây dựng trong bốn luồng $ time make -j4 ... real 1m5.023s user 2m40.270s sys 0m16.809s $ make clean # chạy bản dựng ở chế độ tiêu chuẩn không có song song $ time make ... real 2m6.534s user 1m56.119s sys 0m12 .193s $ make clean # chạy bản dựng với mức song song được chọn tự động $ time make -j ... real 1m5.708s user 2m43.230s sys 0m16.301s

Như bạn có thể thấy, việc sử dụng tính song song (rõ ràng hoặc ẩn) cho phép bạn tăng tốc độ lắp ráp gần như gấp đôi - 1 phút so với 2. Hãy xây dựng dự án tương tự trên bộ xử lý 2 nhân nhanh hơn nhưng với ổ cứng HDD thông thường khá chậm:

$ cat /proc/cpuinfo | head -n10 bộ xử lý: 0 nhà cung cấp_id: Chính hãng Dòng cpu Intel: 6 model: 23 tên model: Pentium(R) CPU lõi kép E6600 @ 3.06GHz bước: 10 cpu MHz: 3066.000 kích thước bộ đệm: 2048 KB ... $ thời gian thực hiện. .. người dùng 0m31.591s thực 0m21.794s sys 0m4.303s $ thời gian tạo -j2 ... người dùng 0m23.629s thực 0m21.013s sys 0m3.278s

Mặc dù tốc độ xây dựng cuối cùng đã tăng gấp 3-4 lần nhưng mức cải thiện về số lượng bộ xử lý chỉ khoảng 20%, vì “mắt xích yếu” ở đây là ổ đĩa chậm, gây ra độ trễ khi ghi một số lượng lớn các dữ liệu nhỏ. tệp dự án .obj.

Ghi chú: Tôi muốn nhắc bạn rằng không phải hội nghị nào cũng làm, chạy thành công trên một bộ xử lý (như trường hợp mặc định hoặc khi chỉ định -j1), cũng sẽ chạy thành công với số lượng bộ xử lý lớn hơn có liên quan. Điều này là do sự gián đoạn trong việc đồng bộ hóa hoạt động trong trường hợp lắp ráp phức tạp. Ví dụ rõ ràng nhất về việc xây dựng không thành công trong trường hợp thực thi song song là việc xây dựng nhân Linux cho một số phiên bản của nhân. Khả năng thực hiện song song làm cần phải được kiểm chứng bằng thực nghiệm đối với dự án đang được lắp ráp. Nhưng trong hầu hết các trường hợp, tính năng này có thể được sử dụng và cho phép bạn tăng tốc quá trình lắp ráp một cách đáng kể!

Nếu phương pháp tăng tốc quá trình lắp ráp này dựa trên thực tế là hiện nay phần lớn các hệ thống là đa bộ xử lý (đa lõi), thì phương pháp tiếp theo lợi dụng thực tế là dung lượng bộ nhớ RAM trên các máy tính hiện đại (2- 4-8 GB) vượt quá đáng kể dung lượng bộ nhớ cần thiết cho mã chương trình biên dịch. Trong trường hợp này, quá trình biên dịch, yếu tố hạn chế chính là việc tạo ra nhiều tệp đối tượng, có thể được di chuyển đến khu vực của một đĩa được tạo đặc biệt (đĩa RAM, tmpfs), nằm trong bộ nhớ:

$ tổng số miễn phí đã sử dụng bộ đệm chia sẻ miễn phí được lưu trong bộ nhớ đệm Mem: 4124164 1516980 2607184 0 248060 715964 -/+ bộ đệm/bộ đệm: 552956 3571208 Hoán đổi: 4606972 0 4606972 $ df -m | grep tmp tmpfs 2014 1 2014 1% /dev/shm

Bây giờ bạn có thể tạm thời chuyển các tập tin của dự án đã lắp ráp sang tmpfs(chúng tôi vẫn đang sử dụng máy chủ NTP từ ví dụ trước), vào thư mục /dev/shm:

$ pwd /dev/shm/ntp-4.2.6p3 $ make -j ... real 0m4.081s người dùng 0m1.710s sys 0m1.149s

Trong trường hợp này, cả hai phương pháp cải thiện hiệu suất đều được sử dụng đồng thời và sự cải thiện so với quá trình biên dịch ban đầu gần như là một mức độ lớn. Đúng, ví dụ này đã được chạy trên một hệ thống có ổ cứng HDD chậm, trong đó việc lắp ráp song song thực tế không mang lại lợi ích gì và cần khoảng 30 giây.

Phương pháp tăng tốc này có thể được áp dụng để xây dựng nhân Linux, như đã đề cập, việc xây dựng song song không hoạt động. Để tận dụng bộ nhớ RAM, hãy sao chép cây nguồn kernel vào thư mục /dev/shm:

$ pwd /dev/shm/linux-2.6.35.i686 $ time make bzImage ... HOSTCC Arch/x86/boot/tools/build BUILD Arch/x86/boot/bzImage Thiết bị gốc là (8, 1) Thiết lập là 13052 byte (được đệm vào 13312 byte). Hệ thống là 3604 kB CRC 418921f4 Kernel: Arch/x86/boot/bzImage đã sẵn sàng (#1) real 9m23.986s user 7m4.826s sys 1m18.529s

Như bạn có thể thấy, việc xây dựng nhân Linux chỉ mất chưa đầy 10 phút, đây là một kết quả tốt bất thường.

Để kết luận, chúng tôi có thể khuyên bạn nên tối ưu hóa cẩn thận các điều kiện lắp ráp dự án cho thiết bị được sử dụng cho việc này và do trong quá trình gỡ lỗi, việc lắp ráp được thực hiện hàng trăm lần, bạn có thể tiết kiệm rất nhiều thời gian!

Xây dựng mô-đun hạt nhân

Một trường hợp đặc biệt của việc xây dựng ứng dụng là xây dựng các mô-đun (trình điều khiển) nhân Linux. Bắt đầu từ phiên bản kernel 2.6, để xây dựng một mô-đun, Makefile, được xây dựng dựa trên việc sử dụng macro và tất cả những gì chúng ta phải làm là viết (đối với một tệp mã của riêng chúng ta có tên là mod_params.c), mẫu sau để lắp ráp các mô-đun:

Liệt kê 1. Makefile để xây dựng các mô-đun hạt nhân
CURRENT = $(shell uname -r) KDIR = /lib/modules/$(CURRENT)/build PWD = $(shell pwd) TARGET = mod_params obj-m:= $(TARGET).o mặc định: $(MAKE) - Mô-đun C $(KDIR) M=$(PWD) ...$ make make -C /lib/modules/2.6.18-92.el5/build \ M=examples/modules-done_1/hello_printk module make: Vào thư mục `/usr/src/kernels/2.6.18-92.el5- i686" CC [M] /examples/modules-done_1/hello_printk/hello_printk.o Xây dựng mô-đun, giai đoạn 2. MODPOST CC /examples/modules-done_1/hello_printk/hello_printk.mod.o LD [M] ví dụ/mô-đun-done_1/ hello_printk/hello_printk.ko make: Rời khỏi thư mục `/usr/src/kernels/2.6.18-92.el5-i686" $ ls -l *.o *.ko -rw-rw-r-- 1 olej olej 74391 Mar 19 15:58 hello_printk.ko -rw-rw-r-- 1 olej olej 42180 19 tháng 3 15:58 hello_printk.mod.o -rw-rw-r-- 1 olej olej 33388 19 tháng 3 15:58 hello_printk.o $ tệp hello_printk.ko hello_printk.ko: ELF 32-bit LSB có thể định vị lại, Intel 80386, phiên bản 1 (SYSV), không bị tước bỏ $ /sbin/modinfo hello_printk.ko tên tệp: hello_printk.ko tác giả: Oleg Tsiliuric giấy phép: GPL srcversion: 83915F228EC39FFCBAF99FD phụ thuộc: vermagic: 2.6.18-92.el5 SMP mod_unload 686 REGPARM 4KSTACKS gcc-4.1

Phần kết luận

Bài viết thảo luận về các khía cạnh khi làm việc với tiện ích make, vốn không thường được mô tả trong tài liệu nhưng có thể cực kỳ hữu ích trong công việc thực tế. Chúng tôi cũng đã hoàn thành cuộc thảo luận về các vấn đề liên quan đến việc phân phối và lắp ráp phần mềm trên hệ điều hành Linux.

Trong bài viết tiếp theo, chúng tôi sẽ bắt đầu giới thiệu về các thư viện API có trong hệ thống POSIX.

Viết một makefile đôi khi có thể trở thành một vấn đề đau đầu. Tuy nhiên, nếu bạn tìm ra cách, mọi thứ sẽ đâu vào đấy và việc viết một tệp tạo tệp mạnh mẽ dài 40 dòng cho bất kỳ dự án lớn nào có thể được thực hiện nhanh chóng và dễ dàng.

Chú ý! Giả định có kiến ​​thức cơ bản về tiện ích tạo GNU.

Chúng tôi có một dự án trừu tượng điển hình với cấu trúc thư mục sau:

Hãy sử dụng cái gì đó như #include để đưa các tệp tiêu đề vào nguồn , nghĩa là thư mục project/include được tạo tiêu chuẩn trong quá trình biên dịch.

Sau khi lắp ráp nó sẽ trông như thế này:

Tức là thư mục bin chứa các phiên bản làm việc (ứng dụng) và debug (application_debug), trong thư mục con Release và Debug của thư mục project/obj cấu trúc của thư mục project/src được lặp lại với mã nguồn tương ứng của các file đối tượng , từ đó nội dung của thư mục bin được biên dịch.

Để đạt được hiệu ứng này, hãy tạo Makefile trong thư mục dự án với nội dung sau:

  1. root_include_dir:= bao gồm
  2. root_source_dir:=src
  3. source_subdirs:= . thư mục1 thư2
  4. biên dịch_flags:= -Tường -MD -pipe
  5. link_flags:= -s -pipe
  6. thư viện:= -ldl
  7. rel_include_dirs:= $(addprefix ../ ../ , $(root_include_dir) )
  8. rel_source_dirs:= $(addprefix ../ ../ $(root_source_dir) / , $(source_subdirs) )
  9. object_dirs:= $(addprefix $(root_source_dir) / , $(source_subdirs) )
  10. đối tượng:= $(patsubst ../ ../% , % , $(ký tự đại diện $(addsuffix /* .c* , $(relative_source_dirs) ) ) )
  11. đối tượng:= $(objects:.cpp=.o)
  12. đối tượng:= $(objects:.c=.o)
  13. tất cả: $(tên_chương trình)
  14. $(program_name) : obj_dirs $(objects)
  15. g++ -o $@ $(objects) $(link_flags) $(libraries)
  16. obj_dirs:
  17. mkdir -p $(objects_dirs)
  18. VPATH:= ../ ../
  19. %.o: %.cpp
  20. g++ -o $@ -c $< $(compile_flags) $(build_flags) $(addprefix -I, $(relative_include_dirs) )
  21. %.o: %.c
  22. g++ -o $@ -c $< $(compile_flags) $(build_flags) $(addprefix -I, $(relative_include_dirs) )
  23. .PHONY: sạch sẽ
  24. lau dọn:
  25. rm -rf bin obj
  26. bao gồm $(ký tự đại diện $(addsuffix /* .d, $(objects_dirs) ) )

Ở dạng thuần túy, một makefile như vậy chỉ hữu ích để đạt được mục tiêu rõ ràng, nó sẽ loại bỏ các thư mục bin và obj.
Hãy thêm một tập lệnh khác có tên Phát hành để xây dựng phiên bản hoạt động:

Mkdir -p bin mkdir -p obj mkdir -p obj/Release make --directory=./obj/Release --makefile=../../Makefile build_flags="-O2 -fomit-frame-pointer" chương trình_name=. ./../bin/ứng dụng

Và một tập lệnh gỡ lỗi khác để xây dựng phiên bản gỡ lỗi:

Mkdir -p bin mkdir -p obj mkdir -p obj/Debug make --directory=./obj/Debug --makefile=../../Makefile build_flags="-O0 -g3 -D_DEBUG" chương trình_name=../ ../bin/application_debug

Việc gọi một trong số họ sẽ tập hợp dự án của chúng ta thành một phiên bản hoạt động hoặc gỡ lỗi. Và bây giờ, điều đầu tiên trước tiên.

Giả sử chúng ta cần xây dựng một phiên bản gỡ lỗi. Chuyển đến thư mục dự án và gọi ./Debug. Ba dòng đầu tiên tạo các thư mục. Ở dòng thứ tư, tiện ích make được thông báo rằng thư mục hiện tại khi khởi động phải là project/obj/Debug, liên quan đến điều này, đường dẫn đến tệp makefile sau đó sẽ được chuyển và hai hằng số được đặt: build_flags (cờ biên dịch quan trọng đối với phiên bản gỡ lỗi được liệt kê ở đây) và tên_chương trình (đối với phiên bản gỡ lỗi - đây là application_debug).

1: Một biến được khai báo với tên thư mục gốc của các tệp tiêu đề.

2: Một biến được khai báo với tên của thư mục gốc nguồn.

3: Một biến có tên thư mục con của thư mục nguồn gốc được khai báo.

4: Một biến có cờ biên dịch chung được khai báo. -MD buộc trình biên dịch tạo một tệp phụ thuộc có cùng tên với phần mở rộng .d cho mỗi nguồn. Mỗi tệp như vậy trông giống như một quy tắc, trong đó mục tiêu là tên nguồn và phần phụ thuộc là tất cả các tệp nguồn và tiêu đề mà nó bao gồm với lệnh #include. Cờ -pipe buộc trình biên dịch sử dụng IPC thay vì hệ thống tập tin, giúp tăng tốc độ biên dịch phần nào.

5: Một biến có cờ bố cục chung được khai báo. -s buộc trình liên kết xóa các phần .symtab, .strtab và một loạt các phần khác có tên như .debug* khỏi tệp ELF thu được, điều này làm giảm đáng kể kích thước của nó. Để gỡ lỗi tốt hơn, khóa này có thể được gỡ bỏ.

6: Một biến được khai báo với tên của các thư viện được sử dụng làm khóa liên kết.

8: Một biến được khai báo chứa tên tương đối của các thư mục có tệp tiêu đề tiêu chuẩn. Sau đó, những tên như vậy được chuyển trực tiếp đến trình biên dịch, trước khóa chuyển -I. Đối với trường hợp của chúng tôi, nó sẽ là ../../include, vì chúng tôi chỉ có một thư mục như vậy. Hàm addprefix thêm đối số đầu tiên của nó vào tất cả các mục tiêu được chỉ định bởi đối số thứ hai.

9: Một biến được khai báo chứa tên tương đối của tất cả các thư mục con của thư mục nguồn gốc. Kết quả là chúng ta nhận được: ../../src/. ../../src/dir1 ../../src/dir1.

10: Một biến được khai báo chứa tên của các thư mục con của thư mục project/obj/Debug/src liên quan đến dự án/obj/Debug hiện tại. Nghĩa là, với điều này, chúng tôi liệt kê một bản sao của cấu trúc thư mục project/src. Kết quả là chúng ta nhận được: /src/dir1 src/dir2.

11: Một biến được khai báo chứa tên của các nguồn được tìm thấy dựa trên các tệp *.c* có cùng tên (.cpp\.c), bất kể thư mục hiện tại. Chúng ta hãy xem xét từng bước một: kết quả của phần bổ sung sẽ là ../../src/./*.с* ../../src/dir1/*.с* ../../src/ dir2/*.с*. Hàm ký tự đại diện sẽ mở rộng các mẫu có dấu hoa thị thành tên tệp thực: ../../src/./main.сpp ../../src/dir1/file1.с../../src/dir1/file2 .сpp ../../src/dir2/file3.с../../src/dir2/file4.с. Hàm patsubsb sẽ xóa tiền tố ../../ khỏi tên tệp (nó thay thế mẫu được đưa ra trong đối số đầu tiên bằng mẫu trong đối số thứ hai và % đại diện cho bất kỳ số lượng ký tự nào). Kết quả là chúng ta nhận được: src/./main.сpp src/dir1/file1.с src/dir1/file2.сpp src/dir2/file3.с src/dir2/file4.с.

12: Trong biến có tên của các nguồn mở rộng, .cpp được thay thế bằng .o.

13: Trong biến có tên của các nguồn mở rộng, .c được thay thế bằng .o.

15: Quy tắc đầu tiên được công bố - mục tiêu của nó trở thành mục tiêu của toàn bộ dự án. Phần phụ thuộc là một hằng số chứa tên của chương trình (../../bin/application_debug chúng tôi đã chuyển nó khi chạy make từ tập lệnh).

17: Mô tả mục tiêu chính. Sự phụ thuộc cũng rất rõ ràng: sự hiện diện của các thư mục con được tạo trong project/obj/Debug, lặp lại cấu trúc của thư mục project/src và nhiều tệp đối tượng trong đó.

18: Hành động liên kết các tập tin đối tượng vào một mục tiêu được mô tả.

20: Quy tắc trong đó mục tiêu là thư mục project/obj/Debug/src và các thư mục con của nó.

21: Hành động để đạt được mục tiêu là tạo các thư mục thích hợp src/., src/dir1 và src/dir2. Công tắc -p của tiện ích mkdir sẽ bỏ qua lỗi nếu khi tạo một thư mục, nó đã tồn tại.

23: Biến VPATH lấy giá trị ../../. Điều này là bắt buộc đối với các mẫu quy tắc sau.

25: Mô tả một bộ quy tắc có mục tiêu là bất kỳ mục tiêu nào phù hợp với mẫu %.o (nghĩa là có tên kết thúc bằng .o) và các phụ thuộc của nó là các mục tiêu có cùng tên khớp với mẫu %.cpp (nghĩa là , có tên kết thúc bằng .cpp). Trong trường hợp này, tên giống nhau không chỉ được hiểu là trùng khớp chính xác mà còn được hiểu nếu tên phụ thuộc được đặt trước nội dung của biến VPATH. Ví dụ: tên src/dir1/file2 và ../../src/dir1/file2 sẽ khớp vì VPATH chứa ../../.

26: Gọi trình biên dịch để biến nguồn C++ thành tệp đối tượng.

28: Mô tả một tập hợp các quy tắc có mục tiêu là bất kỳ mục tiêu nào phù hợp với mẫu %.o (nghĩa là có tên kết thúc bằng .o) và các phụ thuộc của nó là các mục tiêu có cùng tên khớp với mẫu %.c (nghĩa là , có tên kết thúc bằng .c). Cùng tên như ở dòng 23.

29: Gọi trình biên dịch để biến nguồn C thành tệp đối tượng.

31: Một số mục tiêu rõ ràng được tuyên bố là trừu tượng. Việc đạt được mục tiêu trừu tượng luôn diễn ra và không phụ thuộc vào sự tồn tại của tệp cùng tên.

32: Tuyên bố mục tiêu trừu tượng sạch sẽ.

33: Hành động để đạt được điều này là hủy các thư mục project/bin và project/obj cùng với tất cả nội dung của chúng.

36: Bao gồm nội dung của tất cả các tệp phụ thuộc (có phần mở rộng .d) nằm trong thư mục con của thư mục hiện tại. Tiện ích make thực hiện hành động này khi bắt đầu phân tích cú pháp makefile. Tuy nhiên, các tệp phụ thuộc chỉ được tạo sau khi biên dịch. Điều này có nghĩa là trong quá trình xây dựng đầu tiên, sẽ không có một tệp nào như vậy được đưa vào. Nhưng nó không đáng sợ. Mục đích của việc bao gồm các tệp này là buộc biên dịch lại các nguồn phụ thuộc vào tệp tiêu đề đã sửa đổi. Trên bản dựng thứ hai và các bản dựng tiếp theo, make sẽ bao gồm các quy tắc được mô tả trong tất cả các tệp phụ thuộc và, nếu cần, đạt được tất cả các mục tiêu phụ thuộc vào tệp tiêu đề đã sửa đổi.

Đã có lúc, tôi thực sự bỏ lỡ một cuốn sách hướng dẫn như vậy để hiểu những điều cơ bản về chế tạo. Tôi nghĩ nó sẽ thú vị với ít nhất một ai đó. Mặc dù công nghệ này sắp chết nhưng nó vẫn được sử dụng trong nhiều dự án. Không có đủ nghiệp cho trung tâm “Dịch thuật”, ngay khi có cơ hội, tôi cũng sẽ bổ sung vào đó. Đã thêm vào Bản dịch. Nếu có sai sót trong thiết kế, vui lòng chỉ ra. Tôi sẽ sửa nó.

Bài viết sẽ chủ yếu được những người nghiên cứu lập trình C/C++ trên các hệ thống giống UNIX quan tâm từ đầu mà không cần sử dụng IDE.

Biên dịch một dự án bằng tay là một công việc rất tẻ nhạt, đặc biệt là khi có nhiều tệp nguồn và đối với mỗi tệp đó, bạn cần phải nhập các lệnh biên dịch và liên kết mỗi lần. Nhưng mọi chuyện không tệ đến thế. Bây giờ chúng ta sẽ tìm hiểu cách tạo và sử dụng Makefiles. Makefile là một tập hợp các hướng dẫn dành cho chương trình tạo, giúp bạn xây dựng một dự án phần mềm chỉ bằng một cú chạm.

Để thực hành, bạn sẽ cần tạo một dự án cực nhỏ giống như Hello World từ bốn tệp trong một thư mục:

chính.cpp

#bao gồm #include "functions.h" sử dụng không gian tên std; int main())( print_hello(); cout<< endl; cout << "The factorial of 5 is " << factorial(5) << endl; return 0; }


xin chào.cpp

#bao gồm #include "functions.h" sử dụng không gian tên std; void print_hello())( cout<< "Hello World!"; }


giai thừa.cpp

#include "functions.h" int giai thừa(int n)( if(n!=1)( return(n * giai thừa(n-1)); ) else return 1; )


hàm.h

void print_hello(); int giai thừa(int n);


Bạn có thể tải xuống mọi thứ với số lượng lớn từ đây
Tác giả đã sử dụng ngôn ngữ C++, ngôn ngữ này hoàn toàn không cần thiết phải biết và trình biên dịch g++ từ gcc. Bất kỳ trình biên dịch nào khác rất có thể cũng sẽ hoạt động. Các tập tin được sửa chữa một chút để chúng được biên dịch bằng gcc 4.7.1
làm chương trình
Nếu bạn chạy
làm
sau đó chương trình sẽ cố gắng tìm một tệp có tên mặc định Makefile trong thư mục hiện tại và thực hiện các hướng dẫn từ nó. Nếu có một số tệp tạo tệp trong thư mục hiện tại, bạn có thể trỏ đến tệp bạn cần như thế này:
tạo -f MyMakefile
Còn rất nhiều thông số khác mà chúng ta chưa cần. Bạn có thể tìm hiểu về họ trong trang man.
Quá trình xây dựng
Trình biên dịch lấy các tệp mã nguồn và tạo ra các tệp đối tượng từ chúng. Sau đó, trình liên kết lấy các tệp đối tượng và tạo ra tệp thực thi từ chúng. Hội = biên dịch + liên kết.
Biên soạn bằng tay
Cách dễ nhất để xây dựng chương trình:
g++ main.cpp xin chào.cpp giai thừa.cpp -o xin chào
Thật bất tiện khi phải gõ cái này mỗi lần, vì vậy chúng tôi sẽ tự động hóa nó.
Makefile đơn giản nhất
Nó nên chứa các phần sau:
mục tiêu: nhóm phụ thuộc
Ví dụ của chúng tôi, makefile sẽ trông như thế này:
tất cả: g++ main.cpp hello.cpp giai thừa.cpp -o xin chào
Xin lưu ý rằng dòng lệnh phải bắt đầu bằng một tab! Lưu cái này dưới dạng Makefile-1 trong thư mục dự án của bạn và chạy bản dựng với make -f Makefile-1
Trong ví dụ đầu tiên, mục tiêu được gọi là all . Đây là mục tiêu mặc định cho tệp tạo tệp và sẽ được thực thi nếu không có mục tiêu nào khác được chỉ định rõ ràng. Ngoài ra, mục tiêu này trong ví dụ này không có phần phụ thuộc, vì vậy hãy bắt đầu thực hiện lệnh mong muốn ngay lập tức. Và lệnh lần lượt khởi chạy trình biên dịch.
Sử dụng phụ thuộc
Sử dụng nhiều mục tiêu trong một tệp thực hiện rất hữu ích cho các dự án lớn. Điều này là do thực tế là nếu bạn thay đổi một tệp, bạn sẽ không cần phải xây dựng lại toàn bộ dự án mà chỉ có thể xây dựng lại phần đã thay đổi. Ví dụ:
all: xin chào xin chào: main.o giai thừa.o xin chào.o g++ main.o giai thừa.o xin chào.o -o xin chào main.o: main.cpp g++ -c main.cpp giai thừa.o: giai thừa.cpp g++ -c giai thừa.cpp hello.o: hello.cpp g++ -c hello.cpp clean: rm -rf *.o xin chào
Điều này sẽ được lưu dưới tên Makefile-2 trong cùng một thư mục

Bây giờ mục tiêu all chỉ có phần phụ thuộc chứ không có lệnh. Trong trường hợp này, khi được gọi, make sẽ thực thi tuần tự tất cả các phần phụ thuộc được chỉ định trong tệp cho mục tiêu này.
Một mục tiêu mới, sạch sẽ, cũng đã được thêm vào. Theo truyền thống, nó được sử dụng để nhanh chóng dọn dẹp tất cả các kết quả xây dựng của một dự án. Quá trình dọn dẹp được bắt đầu như thế này: make -f Makefile-2 clean

Sử dụng biến và nhận xét
Các biến được sử dụng rộng rãi trong makefiles. Ví dụ, đây là một cách thuận tiện để tính đến khả năng dự án sẽ được biên dịch bằng một trình biên dịch khác hoặc với các tùy chọn khác.
# Đây là chú thích cho biết biến CC chỉ định trình biên dịch được sử dụng để xây dựng CC=g++ # Đây là một chú thích khác. Ông giải thích rằng biến CFLAGS chứa các cờ được chuyển đến trình biên dịch CFLAGS=-c -Wall all: hello hello: main.o Factorial.o hello.o $(CC) main.o Factorial.o hello.o -o hello main .o: main.cpp $(CC) $(CFLAGS) main.cpp giai thừa.o: giai thừa.cpp $(CC) $(CFLAGS) giai thừa.cpp hello.o: hello.cpp $(CC) $(CFLAGS ) hello.cpp sạch: rm -rf *.o xin chào
Đây là Makefile-3
Biến là một thứ rất thuận tiện. Để sử dụng chúng, bạn chỉ cần gán cho chúng một giá trị trước khi sử dụng chúng. Sau đó, bạn có thể thay thế giá trị của chúng vào đúng vị trí theo cách này: $(VAR)
Phải làm gì tiếp theo
Sau hướng dẫn ngắn gọn này, bạn đã có thể thử tự mình tạo các tệp makefile đơn giản. Tiếp theo bạn cần đọc sách giáo khoa và sách hướng dẫn nghiêm túc. Là một hợp âm cuối cùng, bạn có thể thử tự mình tháo rời nó và tạo ra một tệp tạo tệp phổ quát như vậy, có thể điều chỉnh cho hầu hết mọi dự án chỉ bằng hai lần chạm:
CC=g++ CFLAGS=-c -Wall LDFLAGS= SOURCES=main.cpp hello.cpp giai thừa.cpp OBJECTS=$(SOURCES:.cpp=.o) EXECUTABLE=xin chào tất cả: $(SOURCES) $(EXECUTABLE) $(EXECUTABLE ): $(ĐỐI TƯỢNG) $(CC) $(LDFLAGS) $(ĐỐI TƯỢNG) -o $@ .cpp.o: $(CC) $(CFLAGS) $< -o $@
Makefile-4
Chúc may mắn!

Tiện ích này tự động xác định phần nào của một chương trình lớn cần được biên dịch lại và các lệnh để biên dịch lại chúng. Thường xuyên nhất làmđược sử dụng để biên dịch các chương trình C và chứa các tính năng dành riêng cho các tác vụ đó, nhưng bạn có thể sử dụng làm với bất kỳ ngôn ngữ lập trình nào. Hơn nữa, sử dụng tiện ích làm không giới hạn các chương trình. Bạn có thể sử dụng nó để mô tả bất kỳ vấn đề nào trong đó một số tệp phải được tạo tự động từ những tệp khác bất cứ khi nào chúng thay đổi.

tạo tập tin

Trước khi sử dụng làm, bạn cần tạo một tệp có tên tạo tập tin, mô tả mối quan hệ giữa các tệp chương trình của bạn và chứa các lệnh để cập nhật từng tệp. Thông thường, một tệp thực thi phụ thuộc vào các tệp đối tượng, do đó các tệp này lại phụ thuộc vào tệp nguồn và tệp tiêu đề. Đối với tên tạo tập tin tiêu đề đề nghị tệp GNUmake, tạo tập tin hoặc Makefile và việc tìm kiếm diễn ra chính xác theo thứ tự đã chỉ định. Nếu bạn cần sử dụng tên không chuẩn, bạn có thể chuyển nó một cách rõ ràng bằng tùy chọn -f.
Khi tạo tập tinđã được viết sẵn, chỉ cần chạy lệnh trong thư mục chứa nó làm. Đơn giản tạo tập tin bao gồm các quy tắc (hướng dẫn) thuộc loại sau:


BIẾN = GIÁ TRỊ...
MỤC TIÊU...: SỰ PHỤ THUỘC...
ĐỘI 1
ĐỘI 2
BIẾN = GIÁ TRỊ...
MỤC TIÊU...: SỰ PHỤ THUỘC...
ĐỘI 1
ĐỘI 2

vân vân.

MỤC TIÊU thường đại diện cho tên của tệp được tạo bởi chương trình làm; ví dụ về mục tiêu là tệp thực thi hoặc tệp đối tượng. Mục tiêu cũng có thể là tên của hành động cần thực hiện, chẳng hạn như " lau dọn".
NGHIỆN là một tệp mà sự sửa đổi của nó đóng vai trò là dấu hiệu cho thấy mục tiêu là cần thiết. Thường thì mục tiêu phụ thuộc vào một số tệp.
ĐỘI- là hành động được thực hiện làm. Một quy tắc có thể có nhiều lệnh, mỗi lệnh nằm trên một dòng riêng. Lưu ý quan trọng: Bạn phải bắt đầu mỗi dòng chứa lệnh bằng ký tự tab. Các dòng dài được chia thành nhiều dòng bằng dấu gạch chéo ngược, theo sau là dòng mới. Dấu hiệu sắc nét # là sự khởi đầu của một bình luận. Phù hợp với # hoàn toàn bị bỏ qua. Nhận xét có thể trải dài trên nhiều dòng bằng cách sử dụng dấu gạch chéo ngược ở cuối dòng.

Ví dụ về tệp thực hiện

Sử dụng hành động mặc định


#mục tiêu mặc định - chỉnh sửa tệp
chỉnh sửa: main.o kbd.o command.o display.o \
cc -o edit main.o kbd.o command.o display.o \
chèn.o search.o files.o utils.o

Main.o: main.c defs.h
cc -c chính.c
kbd.o: kbd.c defs.h command.h
cc -c kbd.c
command.o: command.c defs.h command.h
cc -c lệnh.c
display.o: display.c defs.h buffer.h
cc -c hiển thị.c
Insert.o: Insert.c defs.h buffer.h
cc -c chèn.c
search.o: search.c defs.h buffer.h
cc -c tìm kiếm.c
files.o: files.c defs.h buffer.h lệnh.h
cc -c tập tin.c
utils.o: utils.c defs.h
cc -c utils.c
lau dọn:
rm chỉnh sửa main.o kbd.o command.o display.o \
chèn.o search.o files.o utils.o

Mặc định, làm bắt đầu bằng quy tắc đầu tiên (không tính các quy tắc có tên mục tiêu bắt đầu bằng " . "). Đây được gọi là mục tiêu mặc định chính. Trong trường hợp của chúng tôi, đây là quy tắc biên tập. Nếu tập tin biên tập mới hơn các tệp đối tượng mà nó phụ thuộc vào thì sẽ không có gì xảy ra. Bằng không thì trước làm có thể xử lý đầy đủ quy tắc này, nó phải xử lý đệ quy các quy tắc cho các tệp mà nó phụ thuộc vào " biên tập". Mỗi tệp này được xử lý theo quy tắc riêng. Việc biên dịch lại phải được thực hiện nếu tệp nguồn hoặc bất kỳ tệp tiêu đề nào được đề cập trong số các tệp phụ thuộc được cập nhật muộn hơn tệp đối tượng hoặc nếu tệp đối tượng không tồn tại.
Luật lệ lau dọn không tương ứng với bất kỳ tệp nào được tạo và theo đó, lau dọn Nó không phụ thuộc vào bất cứ điều gì và bản thân nó cũng không nằm trong danh sách phụ thuộc. Khi khởi chạy theo mặc định lau dọn sẽ không được gọi. Để thực thi nó, bạn phải xác định rõ ràng mục tiêu khi khởi động làm - làm sạch.
Các biến và hành động mặc định (quy tắc ngầm) có thể được sử dụng để rút ngắn mục nhập

Mục đích đặc biệt .PHONYđược xây dựng vào làm và xác định các phụ thuộc của nó là tên mục tiêu không có tệp tương ứng. Nếu bạn bỏ qua quy tắc này thì việc tạo một tệp trong thư mục hiện tại có tên lau dọn sẽ chặn việc thực thi Làm sạch.
Sử dụng quy tắc mặc định cho phép bạn thay đổi kiểu của các mục phụ thuộc:

Dấu ngoặc vuông có nghĩa là sự hiện diện của phần này là tùy chọn.
Mục tiêu- tên của mục tiêu cần hoàn thành.
Biến ="abc"-định nghĩa lại các biến. Giá trị của các biến được nhập vào dòng lệnh có mức độ ưu tiên cao hơn các định nghĩa trong makefile.
Tùy chọn:
-f tập tin- đặt tên rõ ràng tạo tập tin, nếu tác vụ bị bỏ qua thì các tập tin sẽ được tìm kiếm tệp GNUmake, tạo tập tin hoặc Makefile
-N; - bắt chước các hành động mà không thực hiện thực sự, được sử dụng để gỡ lỗi
-t- thay đổi thời gian sửa đổi mục tiêu mà không thực hiện thực tế
-q- kiểm tra nhu cầu cập nhật mục tiêu mà không thực sự thực hiện nó