You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Makefile技巧:让规则依赖y.tab.h但排除在$^变量之外

问题描述

我正在用Lex和Yacc开发编译器,编译main时需要先通过Yacc生成y.tab.h文件,但不希望这个.h文件被包含在$^变量里——因为编译器不接受把.h文件作为输入。

想问两个问题:

  1. 能不能让规则依赖y.tab.h,但把它从$^中排除?
  2. 如果这属于XY问题(即思路本身有问题),有没有更合理的Makefile结构实现目标?

附上当前的Makefile:

targets := ccpy

################################### Options ####################################

cc            := gcc
cFlags        := -c -O3
cFlagsLenient := $(cFlags) -w
cFlagsHard    := $(cFlags) -Wall -pedantic
cFlagsDebug   := -c -W -Wall -pedantic -g -Og
ldFlags       := -lm -ly -ll
ldFlagsDebug  := -g $(ldFlags)

################################### Folders ####################################

binDir   := ./bin
debugDir := ./debug
objDir   := ./obj
srcDir   := ./src
testDir  := ./test

################################ File categories ###############################

mains    := $(targets:%=$(objDir)/%.o)

srcL     := $(wildcard $(srcDir)/*.l)
srcY     := $(wildcard $(srcDir)/*.y)

outL     := $(srcL:$(srcDir)/%.l=$(srcDir)/%.c)
outY     := $(srcY:$(srcDir)/%.y=$(srcDir)/%.c)

objL     := $(outL:$(srcDir)/%.c=$(objDir)/%.o)
objY     := $(outY:$(srcDir)/%.c=$(objDir)/%.o)
cleanY   := $(outY) $(srcY:$(srcDir)/%.y=$(srcDir)/%.h) $(srcDir)/y.output $(srcdir)/y.gv

srcC     := $(filter-out $(outL) $(outY), $(wildcard $(srcDir)/*.c))
objC     := $(srcC:$(srcDir)/%.c=$(objDir)/%.o)

obj      := $(objC) $(objL) $(objY)
objDebug := $(obj:$(objDir)/%.o=$(debugDir)/%.do)

srcTest  := $(wildcard $(testDir)/*.c)
tests    := $(srcTest:$(testDir)/%.c=$(binDir)/%)

#################################### Recipes ###################################

# Basic
all  : $(targets)
again : clean all
debug: $(targets:%=%.db)
test : $(tests)
objects: $(obj)

# Check Variables
info: 
    $(info mains    : $(mains))
    $(info srcC     : $(srcC))
    $(info objC     : $(objC))
    $(info srcL     : $(srcL))
    $(info outL     : $(outL))
    $(info objL     : $(objL))
    $(info srcY     : $(srcY))
    $(info outY     : $(outY))
    $(info objY     : $(objY))
    $(info obj      : $(obj))
    $(info objDebug : $(objDebug))
    $(info srcTest  : $(srcTest))
    $(info tests    : $(tests))

# Final Executables
$(targets): % : $(objDir)/%.o $(obj)
    $(cc) -o $@ $^ $(ldFlags)

# Objects directly compiled from C
$(objC): $(objDir)/%.o : $(srcDir)/%.c
    $(cc) $(cFlagsHard) $^ -o $@

# Objects made by Lex and Yacc
$(objL) $(objY): $(objDir)/%.o : $(srcDir)/%.c
    $(cc) $(cFlagsLenient) $^ -o $@

# Files made by Lex
$(outL): $(srcDir)/%.c : $(srcDir)/%.l $(outY)
    lex -o $@ $<

# Files made by Yacc
$(outY): $(srcDir)/%.c : $(srcDir)/%.y
    yacc -v -d -Wcounterexamples --file-prefix="$(srcDir)/y" $^

# Debug executables (outdated)
$(targets:%=%.db): $(objDebug)
    $(cc) -o $@ $^ $(ldFlagsDebug)

# Debug objects (outdated)
$(objDebug): $(debugDir)/%.do : $(srcDir)/%.c
    $(cc) $(cFlagsDebug) $^ -o $@

# Automated testing
$(tests): $(binDir)/%_test : $(testDir)/%_test.c $(filter-out $(mains), $(obj))
    $(cc) -o $@ $^ $(ldFlags) 
    $@

clean: 
    rm -rf $(objDir)/%.o $(debugDir)/%.do $(targets) $(tests) $(cleanY) $(outL)
解决方案

一、让规则依赖y.tab.h但排除在$^之外的方法

完全可以实现,有两种实用方式:

  1. 使用过滤函数剔除头文件
    如果必须把y.tab.h写在依赖列表里,可以用filter-out函数从$^中移除它:

    $(objDir)/ccpy.o: $(srcDir)/ccpy.c $(srcDir)/y.tab.h
        $(cc) $(cFlagsHard) $(filter-out $(srcDir)/y.tab.h, $^) -o $@
    

    不过这种方式略显冗余,因为编译C文件时,编译器会通过#include自动引入头文件,根本不需要把.h文件作为命令行参数。

  2. 使用仅顺序依赖(Order-only Prerequisites)
    Make支持用|分隔符定义仅顺序依赖——这种依赖只会保证目标构建前先完成依赖的构建,但不会被包含在$^、$+等自动变量中:

    $(objDir)/ccpy.o: $(srcDir)/ccpy.c | $(srcDir)/y.tab.h
        $(cc) $(cFlagsHard) $^ -o $@
    

    这种写法完全符合你的需求:y.tab.h会被优先生成,但不会出现在$^里,编译命令只会拿到C源文件。

二、更优的Makefile结构(解决XY问题)

你的核心需求本质是:确保所有依赖y.tab.h的代码在编译前,y.tab.h已经生成。正确的做法是让需要头文件的目标文件直接依赖它,而非把y.tab.h放到最终链接的依赖链中。以下是关键优化点:

1. 明确Yacc的双产物规则

Yacc的-d参数会同时生成.c和.h文件,所以需要把两者都作为规则的目标,让Make清楚它们的关联:

$(outY) $(srcDir)/y.tab.h: $(srcDir)/%.y
    yacc -v -d -Wcounterexamples --file-prefix="$(srcDir)/y" $^

2. 给需要y.tab.h的目标文件加依赖

比如主程序的目标文件ccpy.o如果包含了y.tab.h,就直接添加依赖:

$(objDir)/ccpy.o: $(srcDir)/ccpy.c $(srcDir)/y.tab.h
    $(cc) $(cFlagsHard) $< -o $@

这里用$<代替$^,只取第一个依赖(即C源文件),彻底避免把头文件传到编译器命令里,这也是编译单个C文件的规范写法。

如果多个C文件都依赖y.tab.h,可以批量处理:

$(objC): $(objDir)/%.o : $(srcDir)/%.c $(srcDir)/y.tab.h
    $(cc) $(cFlagsHard) $< -o $@

3. 修正Lex的依赖

Lex生成的代码通常需要y.tab.h来引用Yacc定义的token,所以Lex的生成规则应该依赖y.tab.h而非Yacc生成的.c文件:

$(outL): $(srcDir)/%.c : $(srcDir)/%.l $(srcDir)/y.tab.h
    lex -o $@ $<

优化后的关键规则片段

# Yacc同时生成.c和.h
$(outY) $(srcDir)/y.tab.h: $(srcDir)/%.y
    yacc -v -d -Wcounterexamples --file-prefix="$(srcDir)/y" $^

# Lex生成依赖y.tab.h
$(outL): $(srcDir)/%.c : $(srcDir)/%.l $(srcDir)/y.tab.h
    lex -o $@ $<

# 主程序目标文件依赖y.tab.h
$(objDir)/ccpy.o: $(srcDir)/ccpy.c $(srcDir)/y.tab.h
    $(cc) $(cFlagsHard) $< -o $@

# 其他C文件依赖y.tab.h
$(objC): $(objDir)/%.o : $(srcDir)/%.c $(srcDir)/y.tab.h
    $(cc) $(cFlagsHard) $< -o $@

# 最终链接规则不变,仅依赖目标文件
$(targets): % : $(objDir)/%.o $(obj)
    $(cc) -o $@ $^ $(ldFlags)

这样调整后,既保证了所有依赖y.tab.h的代码都能在头文件生成后编译,又不会把.h文件传到编译器/链接器的命令行,结构更清晰,也符合Makefile的最佳实践。


内容的提问来源于stack exchange,提问作者Ter Maxima

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 10:18:12