makefile-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWhen creating or modifying Makefiles, follow these principles to ensure they are self-documenting and user-friendly.
在创建或修改Makefile时,请遵循以下原则,确保其具备自文档化特性且易于使用。
1. Default Target Must Be help
help1. 默认目标必须为 help
helpAlways set as the default target so users can discover available commands:
helpmakefile
.DEFAULT_GOAL := helpThis ensures running without arguments shows available targets instead of executing an arbitrary first target.
make始终将 设为默认目标,以便用户能够发现可用的命令:
helpmakefile
.DEFAULT_GOAL := help这样可以确保不带参数运行 时,会显示所有可用目标,而不是执行任意的第一个目标。
make2. Self-Documenting Help Target
2. 自文档化帮助目标
The target should automatically build itself from comments in the Makefile. Use the comment pattern:
help##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
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
模式说明
- - The
target: ## Descriptionmarks the description for that target## - - Creates optional section headers in the help output
##@ Section - The command parses these patterns and formats the output
awk
- -
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
必查项
- is set
.DEFAULT_GOAL := help - target exists
help - target uses the self-documenting awk pattern
help - All targets have comments
## Description
- 已设置
.DEFAULT_GOAL := help - 存在 目标
help - 目标使用了自文档化的awk模式
help - 所有目标都带有 注释
## 描述
Warnings
警告项
- Targets without descriptions (missing )
## - Missing declarations for non-file targets
.PHONY - target is not using
help(won't work with includes)$(MAKEFILE_LIST)
- 目标缺少描述(缺失 )
## - 非文件目标缺少 声明
.PHONY - 目标未使用
help(包含其他Makefile时会失效)$(MAKEFILE_LIST)
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 warningProvide 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 , use this symlink pattern:
~/bin对于需要放在 目录下可用的一次性shell脚本或二进制文件,请使用以下符号链接模式:
~/binVariables
变量定义
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"; \
fimakefile
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"; \
fiCheck 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"; \
fimakefile
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"; \
fiKey Principles
核心原则
- Always use symlinks - Never copy files; symlinks ensure updates are automatic
- Safe removal - Only remove if it's a symlink to prevent accidental deletion
- Idempotent install - Running install multiple times should work
- Fail on conflicts - If a non-symlink file exists, error rather than overwrite
- Create ~/bin if needed - ensures the directory exists
mkdir -p
- 始终使用符号链接 - 不要复制文件;符号链接可确保更新自动生效
- 安全删除 - 仅当目标是符号链接时才删除,防止意外删除文件
- 安装幂等性 - 多次运行安装命令都能正常工作
- 冲突时报错 - 如果存在非符号链接的文件,报错而非覆盖
- 按需创建 ~/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
undefinedmakefile
undefinedmy-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)"
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
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
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)"
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
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
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
undefinedall: help
clean: uninstall
undefined