如何在Makefile中为运行时生成的文件设置通配符目标与依赖?
问题描述
在数据科学项目中,需要用GNU Make构建训练数据流程:
- 下载包含10k+音频文件的zip包
- 解压其中的
.aiff文件到data/train目录 - 用ffmpeg将
.aiff转换为.wav后删除原文件
原Makefile代码如下:
.PHONY: dataset DATA_DIR = data dataset: ${DATA_DIR}/train/%.wav @echo Dataset built! ${DATA_DIR}/train/%.wav: ${DATA_DIR}/train/%.aiff ffmpeg -i $*.aiff $*.wav; rm $*.aiff ${DATA_DIR}/train/%.aiff: ${DATA_DIR}/data.zip unzip $< data/train/*.aiff ${DATA_DIR}/data.zip: # ... download the zip file and place it in ./data...
执行时解压正常,但转换步骤报错,提示%.aiff: No such file or directory——因为Make将%当作字面量处理,而非动态匹配文件名。
后续尝试用目录作为目标实现功能,但违反了Make的最佳实践(目录目标无法追踪内部文件的更新):
dataset: ${DATA_DIR}/train @echo Dataset built! ${DATA_DIR}/train: ${DATA_DIR}/data.zip unzip $< "data/train/*.aiff" for f in $@/*.aiff; do ffmpeg -i "$${f}" "$${f%.aiff}.wav"; rm "$${f}"; done ${DATA_DIR}/data.zip: # ... download the zip file and place it in ./data...
需求:在不知道具体文件名和数量的情况下,让Make自动识别解压后的.aiff文件,生成对应的.wav目标与依赖。
解决方案
核心思路
通过生成文件列表的方式,让Make获取解压后的.aiff文件路径,再动态生成.wav目标。这种方法既遵循Make的最佳实践,又能自动处理大量文件。
完整Makefile实现
.PHONY: dataset clean DATA_DIR = data TRAIN_DIR = $(DATA_DIR)/train # 用于记录解压后的所有.aiff文件路径 AIFF_LIST = $(TRAIN_DIR)/aiff_files.list # 最终目标:依赖所有转换完成的.wav文件 dataset: $(patsubst %.aiff,%.wav,$(shell cat $(AIFF_LIST) 2>/dev/null)) @echo Dataset built successfully! # 单个.aiff转.wav的模式规则:只处理存在的文件,转换后删除原文件 $(TRAIN_DIR)/%.wav: $(TRAIN_DIR)/%.aiff ffmpeg -hide_banner -loglevel error -i $< $@ && rm $< # 解压zip包并生成.aiff文件列表 $(AIFF_LIST): $(DATA_DIR)/data.zip # 解压指定目录下的.aiff文件,避免解压无关内容 unzip $< "*/train/*.aiff" -d $(DATA_DIR) # 生成排序后的文件列表,确保Make目标顺序稳定 find $(TRAIN_DIR) -name "*.aiff" | sort > $@ # 下载zip包的规则(替换为实际下载命令) $(DATA_DIR)/data.zip: # 示例:wget -O $@ https://example.com/dataset.zip @echo "Downloading dataset zip file..." # 清理所有生成的文件 clean: rm -rf $(TRAIN_DIR) $(DATA_DIR)/data.zip
工作流程说明
- 触发顺序:执行
make dataset时,Make首先检查$(AIFF_LIST)是否存在。如果不存在,会先执行下载(若data.zip未生成)、解压规则,同时生成.aiff文件列表。 - 动态生成目标:通过
$(shell cat $(AIFF_LIST))读取文件列表,用patsubst将每个.aiff路径替换为.wav,作为dataset的依赖。 - 单个文件转换:模式规则
$(TRAIN_DIR)/%.wav: $(TRAIN_DIR)/%.aiff会为每个.wav目标匹配对应的.aiff源文件,执行转换并删除原文件。 - 增量更新:如果zip包更新,
$(AIFF_LIST)会重新生成,Make会自动重新转换所有文件;如果单个.aiff文件被修改,只会重新转换对应的.wav。
替代方案(二次扩展)
如果不想生成列表文件,也可以使用.SECONDEXPANSION:实现动态匹配,但第一次执行时需要手动触发解压:
.PHONY: dataset unzip DATA_DIR = data TRAIN_DIR = $(DATA_DIR)/train .SECONDEXPANSION: dataset: $$(wildcard $(TRAIN_DIR)/*.wav) @echo Dataset built! $(TRAIN_DIR)/%.wav: $(TRAIN_DIR)/%.aiff ffmpeg -hide_banner -loglevel error -i $< $@ && rm $< unzip: $(DATA_DIR)/data.zip unzip $< "*/train/*.aiff" -d $(DATA_DIR) $(DATA_DIR)/data.zip: # 下载命令 @echo "Downloading zip file..."
执行时需先运行make unzip,再运行make dataset,否则第一次执行make dataset时因无.wav文件会直接结束。
内容的提问来源于stack exchange,提问作者codersblock
相关产品推荐
相关产品推荐

