运用 Makefile 构建你的 Go 项目

能够遗憾,但不要懊悔。

咱们留在这里,从来不是情不自禁。

——— 而是挑选在这里阅历日子

目录

Makefile 是一个强壮且灵敏的构建东西,具有主动化构建、处理依靠联系、使命办理和跨渠道支撑等长处。经过编写和运用 Makefile,开发者能够简化项意图构建进程,进步开发功率,并完成主动化的构建和发布流程。

在许多开源项目和东西中,Makefile 被广泛挑选作为构建东西。它的灵敏性首要体现在其具有 target(方针)的概念,比较于仅运用 Shell 脚本,Makefile 能够更好地安排和办理构建进程。

此外,Makefile 还能够与其他东西和言语进行集成,例如与 C/C++ 编译器、Go 东西链等配合运用。经过界说适当的规矩和指令,能够完成与其他构建东西的无缝集成,进一步进步构建进程的灵敏性和功率。

本文旨在帮助读者了解如何运用 Makefile 东西来构建你的 Go 项目。

  1. 根本介绍
  2. Makefile 的优势
  3. Makefile 的开展前史
  4. Makefile 与 Shell 比照
  5. Makefile 的数据类型
  6. 正式开端
  7. 举个例子

根本介绍

Makefile 是由 GNU Make 东西解析履行的装备文件。要调用 Makefile,需求在指令行中运用 make 指令,并指定要履行的方针或规矩。下面是 Makefile 的根本语法和调用方法的介绍。

创立 Makefile 文件

在项目目录下创立名为 Makefile 的文件,或许运用其他自界说的文件名(例如 makefileGNUmakefile)。

界说规矩和方针

Makefile 由一系列规矩组成,每个规矩由一个方针(target)、依靠项(prerequisites)和指令(commands)组成。方针是需求生成的文件或履行的操作,依靠项是生成方针所需求的文件或其他方针,指令是履行生成方针的操作。

语法格局:

target: prerequisites
    commands

示例:

hello: main.o utils.o
    gcc main.o utils.o -o hello
main.o: main.c
    gcc -c main.c
utils.o: utils.c
    gcc -c utils.c

Makefile 中,方针的命名选用 “蛇型”snake_case)更为常见和推荐。这是由于在 LinuxUnix 体系中,文件名一般运用小写字母和下划线,而不是 “驼峰命名法”“中横线命名法”

调用 Makefile

在指令行中运用 make 指令调用 Makefile,并指定要履行的方针。假如未指定方针,默认会履行 Makefile 中的第一个方针。

语法格局:

    make [target]

示例:

    make hello

上述指令会履行 Makefilehello 方针下界说的指令,编译源代码并生成可履行文件 hello

其他常用选项

-f <filename>:指定要运用的 Makefile 文件名,例如 make -f mymakefile

-C <directory>:指定 Makefile 的作业目录,例如 make -C src

Makefile 的优势

Makefile 是一种便利的主动化构建东西,具有以下长处:

  1. 主动化构建:经过界说好的规矩和方针,Makefile 能够主动履行代码生成、格局校验和编译打包等使命,然后减少了手动操作的作业量。运用简略的指令 make 能够触发整个构建进程,并主动处理依靠联系,只构建必要的部分,进步了构建功率和开发者的作业功率。
  2. 跨渠道支撑:Makefile 是一种通用的构建东西,能够在不同的操作体系上运转,如 LinuxmacOSWindows 等。这为项目供给了更大的灵敏性和可移植性,使得开发者能够在不同渠道上进行构建和部署,无需忧虑渠道差异导致的构建问题。
  3. 标准性和可读性:Makefile 运用结构化的语法和规矩来界说构建进程,使得项意图构建逻辑愈加明晰和易于了解。它供给了变量、条件句子、循环和函数等功用,使得构建脚本具有良好的可读性和可保护性。开发者能够经过编写标准的 Makefile,进步代码的可保护性和团队协作功率。
  4. 契合社区习气:在开源社区中,Makefile 是一种常见的构建东西,被广泛应用于各种项目。许多开源软件开发者习气运用 Makefile 来办理构建和发布流程,这使得开发者能够更轻松地参加和奉献到这些项目中。挑选运用 Makefile 能够使项目与社区保持一致,更易于了解和承受。

综上所述,Makefile 是一种强壮且灵敏的构建东西,它具有主动化构建、跨渠道支撑、标准性和可读性以及契合社区习气等长处。运用 Makefile 能够简化项意图构建进程,进步开发功率,并与开源社区保持一致,使得项目更易于办理和保护。

Makefile 的开展前史

Makefile 的开展前史能够追溯到上世纪70年代。下面是 Makefile 的首要里程碑和开展阶段:

  1. 前期阶段(1970s-1980s):Makefile 最早出现在贝尔实验室的 Unix 体系中,并作为构建东西用于编译和链接软件。前期的 Makefile 是基于 Make 东西的语法,用于描绘源代码文件之间的依靠联系,经过规矩界说了编译和链接的进程。
  2. GNU Make 的出现(1980s-1990s):GNU MakeGNU 项目开发的一款强壮的构建东西,取代了前期的 Make 东西。GNU Make 引入了更多功用和特性,如变量、条件判别、循环等,使得 Makefile 愈加灵敏和可装备。
  3. 跨渠道的运用(2000s-至今):随着开源软件的遍及和多渠道开发的需求,Makefile 的运用逐步扩展到不同的操作体系和编程言语中。Makefile 成为跨渠道构建东西的标准之一,广泛应用于各种开源项目。
  4. 扩展功用和东西链整合:随着软件开发流程的不断演进,Makefile 逐步引入了更多的功用和东西链的整合。例如,经过 Makefile 能够进行代码生成、运转测验、打包发布、部署等更复杂的构建使命,并与其他东西(如编译器、测验结构、继续集成东西等)进行集成。

总体而言,Makefile 的开展前史能够看作是不断演化和改善的进程,以适应不断变化的软件开发需求。它成为了构建软件的标准东西之一,并在跨渠道开发和开源社区中得到广泛应用。

Makefile 与 Shell 比照

Makefile 的语法与 Shell 脚本有类似之处,但它们是不同的言语。MakefileGNU Make 东西的装备文件,用于界说和办理项意图构建规矩。它运用一组特定的语法规矩、指令和 Make 东西供给的内置函数和变量。

Makefile 中,指令一般以 Tab 键开头,并在每行的结束增加分号 (;) 或换行符。这些指令由 Make 东西履行,用于构建项目或履行特定使命。与之不同,Shell 脚本是一种编程言语,用于编写指令行脚本。它运用 Shell 解释器履行,用于履行体系指令、操作文件和控制流程等。

尽管 Makefile 的语法与 Shell 脚本类似,但它们具有不同的用途和特定的语法规矩。Makefile 用于构建项目和办理依靠联系,而 Shell 脚本用于编写体系级使命和主动化操作。因而,在挑选东西和言语时,请留意它们之间的区别,并依据使命的需求挑选适合的东西。

尽管 Shell 脚本和其他构建东西(如 PythonsetuptoolsCMake 等)也能够用于构建开源软件,但 Makefile 供给了一种简略、通用且被广泛承受的构建方法,因而在开源软件中被广泛选用。

当然,挑选运用何种构建东西仍应依据具体项意图需求和开发团队的偏好来决议。

Makefile 的数据类型

Makefile 中,数据类型并不像常见编程言语那样严格界说。Makefile 中的变量能够存储字符串,而且能够进行字符串操作和替换。下面是一些常见的 Makefile 中的数据类型和特性:

  1. 字符串(String):变量能够存储字符串,如 VAR := hello
  2. 列表(List):经过运用空格分隔的值来界说一个列表,如 LIST := item1 item2 item3。列表能够用于遍历、迭代和批量操作。
  3. 函数(Function):Makefile 供给了一些内置函数,能够在变量中进行字符串操作、替换和转化等操作。例如,$(subst from,to,text) 函数能够将字符串中的某个部分替换为另一个部分。
  4. 条件句子:Makefile 支撑条件句子,如 if-else 条件判别。能够依据变量的值或其他条件来履行不同的操作。
  5. 数值型数据:尽管 Makefile 不支撑直接界说数值型变量,但能够运用字符串来表明数值,并在需求时进行转化。

需求留意的是,Makefile 是一种构建东西的描绘言语,其首要意图是界说和履行构建规矩,而不是处理复杂的数据结构和算法。因而,Makefile 的数据类型相对较简略,首要集中在字符串和列表操作上。

正式开端

字符串输出

# 请保证在每一行指令前面运用实践的 Tab 键而不是空格。这是 Makefile 的语法要求。
# 假如在创立 Makefile 时运用了空格而不是 Tab 键,将会导致语法过错。
hello:
    @echo "Hello, World!"

运用变量

Makefile 中,变量的界说需求运用 := 进行赋值操作。在运用变量时,运用 $(VAR_NAME) 的语法来引用变量。

# 界说变量
GREETING := "Hello, World!"
# 输出变量
variable:
    @echo "$(GREETING)"

Makefile 中,?= 是一个预界说的变量赋值方法,被称为 “延迟求值”(Lazy Evaluation)。

具体来说,这个符号用于设置一个变量的默认值,只有当该变量没有被显式设置时才会运用默认值。假如变量现已被设置了,那么 ?= 将不会起作用,而是保留原来的值。

# 设置编译器
GO ?= go

拜访数组

# 界说一个包括多个值的变量
FRUITS := apple orange banana
# 拜访列表中的元素
first := $(firstword $(FRUITS))
second := $(word 2, $(FRUITS))
third := $(word 3, $(FRUITS))
last := $(lastword $(FRUITS))
# 输出列表中的元素
array:
    @echo "First: $(first)"
    @echo "Second: $(second)"
    @echo "Third: $(third)"
    @echo "Last: $(last)"

遍历数组

# 界说一个包括多个值的变量
FRUITS := apple orange banana
# 打印数组的每个元素
print:
    @for fruit in $(FRUITS); do \
        echo "$$fruit"; \
    done

遍历+条件

# 界说一个包括多个值的变量
FRUITS := apple orange banana
# 打印数组的每个元素
filter:
    @for fruit in $(FRUITS); do \
        if [ "$$fruit" = "orange" ]; then \
            echo "$$fruit is my favorite fruit!"; \
        elif [ "$$fruit" = "apple" ]; then \
            echo "$$fruit is my secondary fruit of choice!"; \
        else \
            echo "The fruit I hate the most - $$fruit!"; \
        fi \
    done

判别是否等于

# 界说变量
FRUIT := apple
# 留意:ifeq 是界说在 Makefile 文件的顶层范围,而不是界说在方针规矩中,也就是说,写在 fruit 内是不被答应的
ifeq ($(FRUIT), apple)
    favorite := "It's an apple!"
else ifeq ($(FRUIT), orange)
    favorite := "It's an orange!"
else ifeq ($(FRUIT), banana)
    favorite := "It's a banana!"
else
    favorite := "Unknown fruit!"
endif
# 判别变量的值
fruit:
    @echo $(favorite)

判别是否界说

# 查看变量是否已界说
DEBUG :=
ifdef DEBUG
    MESSAGE := "Debug is defined"
else
    MESSAGE := "Debug is undefined"
endif
# 打印音讯
print_message:
    @echo $(MESSAGE)

嵌入 Python

hello_world := Hello World
python:
    $(eval WORDS := $(shell python3 -c 'import sys; print(sys.argv[1].split())' "$(hello_world)"))
    @echo $(WORDS)" !"

伪方针

Makefile 中,.PHONY 是一个特别的方针(Target),用于声明指定的方针是“伪方针”(Phony Target)。它不表明一个物理文件或途径,而仅仅是一个逻辑方针。因而,当履行这个方针时,Makefile 不会查看是否存在对应的文件,而直接履行该方针下界说的指令。

一般情况下,运用 .PHONY 是为了防止与同名文件产生冲突,或许为了在构建时强制从头履行某些操作。例如,在以下示例中:

.PHONY: clean
clean:
    rm -rf *.o *.out

.PHONY: clean 表明 clean 是一个伪方针,不需求查看是否存在 clean 文件。假如没有这个声明,履行 make clean 指令时,可能会出现如下过错提示:

make: 'clean' is up to date.

由于 Makefile 会认为 clean 现已被构建过了,所以不再履行 rm -rf *.o *.out 指令。

伪方针写在哪?

.PHONY 方针一般放在 Makefile 的顶部或底部,这样做有以下几个好处:

  1. 易于查找和辨认:放在顶部或底部能够便利地找到一切伪方针。
  2. 代码标准和可读性:依照惯例,Makefile 的第一行应该是文件注释(File Comment),用于供给文件的概述、作者、版别等信息。因而,将 .PHONY 放在 Makefile 的第二行或之后能够使文件更契合代码标准和可读性要求。
  3. 防止误解和过错:假如将 .PHONY 放在中间某处,可能会导致 Makefile 中的其他方针被误认为是实践存在的文件,然后引发构建过错或其他问题。

总归,.PHONY 方针放在 Makefile 的顶部或底部都是能够的,可是建议放在顶部,以便更便利地查找和阅览。

依靠构建

$(BUILD_DIR) 是一个 Makefile 变量,在这里表明构建目录的途径,例如 ./build/

mkdir -p $@ 是一个 Shell 指令,用于创立指定的目录。其间,-p 参数表明递归创立子目录,假如目录现已存在则不会报错也不会覆盖原有文件。

$@ 是一个主动化变量,表明当时方针的称号,这里是 $(BUILD_DIR)。当这个方针被履行时,Makefile 会将其解析为 Shell 指令 mkdir -p ./build/,然后创立指定的构建目录。

这种写法常用于界说 Makefile 的方针(Target),它能够保证所需的目录在履行后存在,而且不需求手动创立。例如:

.PHONY: build
BUILD_DIR := ./build/
OUTPUT_DIR := ./output/
build: $(BUILD_DIR)
    go build -o $(OUTPUT_DIR)/bin/app main.go
$(BUILD_DIR):
    mkdir -p $@

在这个示例中,build 方针依靠于 $(BUILD_DIR) 构建目录,也就是说,在履行 build 方针之前,必须先创立 $(BUILD_DIR) 目录。假如 $(BUILD_DIR) 现已存在,则直接越过该进程。

经过这种方法,咱们能够在 Makefile 中界说多个方针,并经过依靠联系和主动化变量来办理它们之间的联系和依靠,然后构建一个完好的构建流程。

举个例子

脚本示例

疏忽某些目录(或文件)后遍历项目目录

方法1

# 设置要扫除的目录列表
EXCLUDE_DIRS := \
    ./vendor \
    ./.git \
    ./.idea \
    ./examples \
    ./test
# 增加匹配的子目录到扫除的目录列表中
EXCLUDE_DIRS += $(foreach dir,$(EXCLUDE_DIRS),$(dir)/*)
# 查找一切非扫除目录的目录
SRC_DIRS := $(shell find . -type d $(foreach dir,$(EXCLUDE_DIRS),! -path "$(dir)"))
# 在这里增加要履行的指令
print_dirs:
    @for dir in $(SRC_DIRS); do \
        echo "Processing directory: $$dir"; \
        (cd $$dir && pwd); \
    done

方法2

# 设置要扫除的目录列表
EXCLUDE_DIRS := \
    ./vendor \
    ./.git \
    ./.idea \
    ./examples \
    ./test
# 增加匹配的子目录到扫除的目录列表中
EXCLUDE_DIRS += $(foreach dir,$(EXCLUDE_DIRS),$(dir)/*)
# 查找一切非扫除目录的目录(界说函数)
define find_src_dirs
    $(shell find . -type d $(foreach dir,$(EXCLUDE_DIRS),! -path "$(dir)"))
endef
# 打印非扫除目录的目录
print_dirs:
    @$(foreach dir,$(call find_src_dirs), \
        (cd $(shell pwd)/$(dir) && pwd); \
    )

方法3

# 显示目录的序号
print_dirs:
    @count=0; \
    @for dir in $(SRC_DIRS); do \
        count=$$((count + 1)); \
        echo " $$(printf "%02d" $$count) - Checking: $$dir"; \
    done

实践场景

项目目录结构

➜ tree
.
├── Makefile
└── scripts
   └── start.sh

start.sh 文件

#!/usr/bin/env bash
# 可在恣意目录位置进行 sh 履行
curdir=`dirname $(readlink -f $0)`
basedir=`dirname $curdir`"/"
# 履行 make generate 指令时,运用 --no-builtin-rules 参数来禁用内置规矩,这有时能够处理一些奇怪的行为。
make --directory ${basedir} --no-builtin-rules generate
#EOF

Makefile 文件

一个基础的示例:

# TBD...
# 设置变量
GOCMD := go
GOBUILD := $(GOCMD) build
GOCLEAN := $(GOCMD) clean
GOTEST := $(GOCMD) test
GODEPS := $(GOCMD) mod download
GOGENERATE := $(GOCMD) generate
GOLINTER := golangci-lint run
BINARY_NAME := yourprojectname
MAIN_FILE := main.go
# 设置要扫除的目录列表(依据实践情况更改)
EXCLUDE_DIRS := ./vendor ./.git ./.idea ./examples ./test
# 查找一切非扫除目录的目录
SRC_DIRS := $(shell find . -type d $(foreach dir,$(EXCLUDE_DIRS),-not -path "$(dir)*"))
# 构建方针:生成代码
generate:
    @for dir in $(SRC_DIRS); do \
        echo "Generating code in directory: $$dir"; \
        (cd $$dir && $(GOGENERATE) -v); \
    done
# 构建方针:代码格局检测
lint:
    $(GOLINTER) ./...
# 构建方针:运转测验
test:
    $(GOTEST) ./...
# 构建方针:编译代码
build:
    $(GOBUILD) -o $(BINARY_NAME) $(MAIN_FILE)
# 构建方针:整理项目
clean:
    $(GOCLEAN)
    rm -f $(BINARY_NAME)
# 构建方针:安装依靠
deps:
    $(GODEPS)
# 构建方针:履行一切构建进程
all: generate lint test build
# 声明一切方针,保证它们被视为伪方针而不是实践的文件名
.PHONY: generate lint test build clean deps all

上述代码是一个示例的 Makefile 文件,用于构建一个 Go 项目。下面对其间的构建方针进行说明:

  • 生成代码(generate):经过遍历 SRC_DIRS 中的目录,履行 go generate 指令来生成代码。在每个目录中履行生成代码指令之前,会先输出要生成代码的目录信息。
  • 代码格局检测(lint):履行 golangci-lint 东西对代码进行格局检测。在这个示例中,运用 $(GOLINTER) 表明履行 golangci-lint run 指令。
  • 运转测验(test):履行 go test 指令来运转项目中的测验。
  • 编译代码(build):运用 go build 指令编译项意图代码,并指定输出的可履行文件称号为 $(BINARY_NAME)
  • 整理项目(clean):履行 go clean 指令来整理项目,并删除生成的可履行文件。
  • 安装依靠(deps):履行 go mod download 指令来下载项意图依靠。
  • 履行一切构建进程(all):经过依次履行 generatelinttestbuild 方针来履行一切的构建进程。
  • 声明一切方针为伪方针:经过 .PHONY 指令声明一切的方针,保证它们被视为伪方针而不是实践的文件名。

这个示例的 Makefile 文件供给了一些常见的构建方针,使得能够经过简略的指令来履行不同的构建操作,如生成代码、代码格局检测、运转测验、编译代码、整理项目等。依据实践需求,能够依据这个示例进行修改和扩展。