makefile-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
When creating or modifying Makefiles, follow these principles to ensure they are self-documenting and user-friendly.
在创建或修改Makefile时,请遵循以下原则,确保其具备自文档化特性且易于使用。

1. Default Target Must Be
help

1. 默认目标必须为
help

Always set
help
as the default target so users can discover available commands:
makefile
.DEFAULT_GOAL := help
This ensures running
make
without arguments shows available targets instead of executing an arbitrary first target.
始终将
help
设为默认目标,以便用户能够发现可用的命令:
makefile
.DEFAULT_GOAL := help
这样可以确保不带参数运行
make
时,会显示所有可用目标,而不是执行任意的第一个目标。

2. Self-Documenting Help Target

2. 自文档化帮助目标

The
help
target should automatically build itself from comments in the Makefile. Use the
##
comment pattern:
makefile
target-name: ## Description of what this target does
	@command here

help: ## Show this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
help
目标应能根据Makefile中的注释自动生成。请使用
##
注释模式:
makefile
target-name: ## Description of what this target does
	@command here

help: ## Show this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

Pattern Explanation

模式说明

  • target: ## Description
    - The
    ##
    marks the description for that target
  • ##@ Section
    - Creates optional section headers in the help output
  • The
    awk
    command parses these patterns and formats the output
  • target: ## 描述
    -
    ##
    用于标记该目标的描述信息
  • ##@ 分组名称
    - 在帮助输出中创建可选的分组标题
  • awk
    命令负责解析这些模式并格式化输出内容

Benefits

优势

  • Documentation stays with the code
  • Adding new targets automatically updates help
  • No manual maintenance of help text
  • Consistent format across all Makefiles
  • 文档与代码共存
  • 新增目标时帮助信息自动更新
  • 无需手动维护帮助文本
  • 所有Makefile的格式保持一致

Audit Mode

审计模式

When asked to audit a Makefile, check for the following issues and report findings:
当需要审计Makefile时,请检查以下问题并报告结果:

Required

必查项

  • .DEFAULT_GOAL := help
    is set
  • help
    target exists
  • help
    target uses the self-documenting awk pattern
  • All targets have
    ## Description
    comments
  • 已设置
    .DEFAULT_GOAL := help
  • 存在
    help
    目标
  • help
    目标使用了自文档化的awk模式
  • 所有目标都带有
    ## 描述
    注释

Warnings

警告项

  • Targets without descriptions (missing
    ##
    )
  • Missing
    .PHONY
    declarations for non-file targets
  • help
    target is not using
    $(MAKEFILE_LIST)
    (won't work with includes)
  • 目标缺少描述(缺失
    ##
    )
  • 非文件目标缺少
    .PHONY
    声明
  • help
    目标未使用
    $(MAKEFILE_LIST)
    (包含其他Makefile时会失效)

Report Format

报告格式

Makefile Audit: <path>

PASS: .DEFAULT_GOAL set to help
PASS: help target exists
FAIL: 3 targets missing ## descriptions: build, test, deploy
WARN: Missing .PHONY for: build, test, clean

Summary: 2 passed, 1 failed, 1 warning
Provide specific line numbers and suggested fixes for each issue found.
Makefile Audit: <path>

PASS: .DEFAULT_GOAL set to help
PASS: help target exists
FAIL: 3 targets missing ## descriptions: build, test, deploy
WARN: Missing .PHONY for: build, test, clean

Summary: 2 passed, 1 failed, 1 warning
请为每个发现的问题提供具体的行号和修复建议。

3. Install/Uninstall Pattern for Scripts and Binaries

3. 脚本与二进制文件的安装/卸载模式

For one-off shell scripts or binaries that should be available in
~/bin
, use this symlink pattern:
对于需要放在
~/bin
目录下可用的一次性shell脚本或二进制文件,请使用以下符号链接模式:

Variables

变量定义

makefile
SCRIPT_NAME := my-script.sh
INSTALL_DIR := $(HOME)/bin
INSTALL_PATH := $(INSTALL_DIR)/$(SCRIPT_NAME)
SOURCE_PATH := $(CURDIR)/$(SCRIPT_NAME)
makefile
SCRIPT_NAME := my-script.sh
INSTALL_DIR := $(HOME)/bin
INSTALL_PATH := $(INSTALL_DIR)/$(SCRIPT_NAME)
SOURCE_PATH := $(CURDIR)/$(SCRIPT_NAME)

Install Target

安装目标

makefile
install: ## Install symlink to ~/bin
	@mkdir -p $(INSTALL_DIR)
	@if [ -L "$(INSTALL_PATH)" ]; then \
		echo "Removing existing symlink..."; \
		rm -f "$(INSTALL_PATH)"; \
	elif [ -e "$(INSTALL_PATH)" ]; then \
		echo "Error: $(INSTALL_PATH) exists and is not a symlink"; \
		exit 1; \
	fi
	@ln -s "$(SOURCE_PATH)" "$(INSTALL_PATH)"
	@echo "Installed: $(INSTALL_PATH) -> $(SOURCE_PATH)"
makefile
install: ## Install symlink to ~/bin
	@mkdir -p $(INSTALL_DIR)
	@if [ -L "$(INSTALL_PATH)" ]; then \
		echo "Removing existing symlink..."; \
		rm -f "$(INSTALL_PATH)"; \
	elif [ -e "$(INSTALL_PATH)" ]; then \
		echo "Error: $(INSTALL_PATH) exists and is not a symlink"; \
		exit 1; \
	fi
	@ln -s "$(SOURCE_PATH)" "$(INSTALL_PATH)"
	@echo "Installed: $(INSTALL_PATH) -> $(SOURCE_PATH)"

Uninstall Target

卸载目标

makefile
uninstall: ## Remove symlink from ~/bin
	@if [ -L "$(INSTALL_PATH)" ]; then \
		rm -f "$(INSTALL_PATH)"; \
		echo "Removed: $(INSTALL_PATH)"; \
	elif [ -e "$(INSTALL_PATH)" ]; then \
		echo "Error: $(INSTALL_PATH) exists but is not a symlink (not removing)"; \
		exit 1; \
	else \
		echo "Nothing to remove: $(INSTALL_PATH) does not exist"; \
	fi
makefile
uninstall: ## Remove symlink from ~/bin
	@if [ -L "$(INSTALL_PATH)" ]; then \
		rm -f "$(INSTALL_PATH)"; \
		echo "Removed: $(INSTALL_PATH)"; \
	elif [ -e "$(INSTALL_PATH)" ]; then \
		echo "Error: $(INSTALL_PATH) exists but is not a symlink (not removing)"; \
		exit 1; \
	else \
		echo "Nothing to remove: $(INSTALL_PATH) does not exist"; \
	fi

Check Target (Optional)

检查目标(可选)

makefile
check: ## Check installation status
	@echo "Source: $(SOURCE_PATH)"
	@if [ -L "$(INSTALL_PATH)" ]; then \
		echo "Status: Installed (symlink)"; \
		echo "Target: $$(readlink "$(INSTALL_PATH)")"; \
	elif [ -e "$(INSTALL_PATH)" ]; then \
		echo "Status: Exists but NOT a symlink"; \
	else \
		echo "Status: Not installed"; \
	fi
makefile
check: ## Check installation status
	@echo "Source: $(SOURCE_PATH)"
	@if [ -L "$(INSTALL_PATH)" ]; then \
		echo "Status: Installed (symlink)"; \
		echo "Target: $$(readlink "$(INSTALL_PATH)")"; \
	elif [ -e "$(INSTALL_PATH)" ]; then \
		echo "Status: Exists but NOT a symlink"; \
	else \
		echo "Status: Not installed"; \
	fi

Key Principles

核心原则

  1. Always use symlinks - Never copy files; symlinks ensure updates are automatic
  2. Safe removal - Only remove if it's a symlink to prevent accidental deletion
  3. Idempotent install - Running install multiple times should work
  4. Fail on conflicts - If a non-symlink file exists, error rather than overwrite
  5. Create ~/bin if needed -
    mkdir -p
    ensures the directory exists
  1. 始终使用符号链接 - 不要复制文件;符号链接可确保更新自动生效
  2. 安全删除 - 仅当目标是符号链接时才删除,防止意外删除文件
  3. 安装幂等性 - 多次运行安装命令都能正常工作
  4. 冲突时报错 - 如果存在非符号链接的文件,报错而非覆盖
  5. 按需创建 ~/bin 目录 -
    mkdir -p
    确保目录存在

For Sourced Scripts

针对需source的脚本

If the script needs to be sourced (not executed), add post-install instructions:
makefile
install: ## Install symlink to ~/bin
	@mkdir -p $(INSTALL_DIR)
	# ... symlink creation ...
	@echo ""
	@echo "Add to your shell config:"
	@echo "  source ~/bin/$(SCRIPT_NAME)"
如果脚本需要被source(而非直接执行),请添加安装后说明:
makefile
install: ## Install symlink to ~/bin
	@mkdir -p $(INSTALL_DIR)
	# ... symlink creation ...
	@echo ""
	@echo "Add to your shell config:"
	@echo "  source ~/bin/$(SCRIPT_NAME)"

Complete Example

完整示例

makefile
undefined
makefile
undefined

my-tool Makefile

my-tool Makefile

SCRIPT_NAME := my-tool.sh INSTALL_DIR := $(HOME)/bin INSTALL_PATH := $(INSTALL_DIR)/$(SCRIPT_NAME) SOURCE_PATH := $(CURDIR)/$(SCRIPT_NAME)
.PHONY: help install uninstall check lint test all clean
.DEFAULT_GOAL := help
##@ General
help: ## Show this help message @awk 'BEGIN {FS = ":.##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Installation
install: ## Install symlink to ~/bin @mkdir -p $(INSTALL_DIR) @if [ -L "$(INSTALL_PATH)" ]; then
echo "Removing existing symlink...";
rm -f "$(INSTALL_PATH)";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Error: $(INSTALL_PATH) exists and is not a symlink";
exit 1;
fi @ln -s "$(SOURCE_PATH)" "$(INSTALL_PATH)" @echo "Installed: $(INSTALL_PATH) -> $(SOURCE_PATH)"
uninstall: ## Remove symlink from ~/bin @if [ -L "$(INSTALL_PATH)" ]; then
rm -f "$(INSTALL_PATH)";
echo "Removed: $(INSTALL_PATH)";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Error: $(INSTALL_PATH) exists but is not a symlink (not removing)";
exit 1;
else
echo "Nothing to remove: $(INSTALL_PATH) does not exist";
fi
check: ## Check installation status @echo "Source: $(SOURCE_PATH)" @if [ -L "$(INSTALL_PATH)" ]; then
echo "Status: Installed (symlink)";
echo "Target: $$(readlink "$(INSTALL_PATH)")";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Status: Exists but NOT a symlink";
else
echo "Status: Not installed";
fi
##@ Development
lint: ## Run shellcheck on scripts shellcheck $(SCRIPT_NAME)
test: ## Run tests ./test.sh
SCRIPT_NAME := my-tool.sh INSTALL_DIR := $(HOME)/bin INSTALL_PATH := $(INSTALL_DIR)/$(SCRIPT_NAME) SOURCE_PATH := $(CURDIR)/$(SCRIPT_NAME)
.PHONY: help install uninstall check lint test all clean
.DEFAULT_GOAL := help
##@ General
help: ## Show this help message @awk 'BEGIN {FS = ":.##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Installation
install: ## Install symlink to ~/bin @mkdir -p $(INSTALL_DIR) @if [ -L "$(INSTALL_PATH)" ]; then
echo "Removing existing symlink...";
rm -f "$(INSTALL_PATH)";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Error: $(INSTALL_PATH) exists and is not a symlink";
exit 1;
fi @ln -s "$(SOURCE_PATH)" "$(INSTALL_PATH)" @echo "Installed: $(INSTALL_PATH) -> $(SOURCE_PATH)"
uninstall: ## Remove symlink from ~/bin @if [ -L "$(INSTALL_PATH)" ]; then
rm -f "$(INSTALL_PATH)";
echo "Removed: $(INSTALL_PATH)";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Error: $(INSTALL_PATH) exists but is not a symlink (not removing)";
exit 1;
else
echo "Nothing to remove: $(INSTALL_PATH) does not exist";
fi
check: ## Check installation status @echo "Source: $(SOURCE_PATH)" @if [ -L "$(INSTALL_PATH)" ]; then
echo "Status: Installed (symlink)";
echo "Target: $$(readlink "$(INSTALL_PATH)")";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Status: Exists but NOT a symlink";
else
echo "Status: Not installed";
fi
##@ Development
lint: ## Run shellcheck on scripts shellcheck $(SCRIPT_NAME)
test: ## Run tests ./test.sh

Stubs to satisfy checkmake minphony rule

Stubs to satisfy checkmake minphony rule

all: help clean: uninstall
undefined
all: help clean: uninstall
undefined