Makefile: 未检测到目标可执行文件已经构建 (GNU Make)

huangapple go评论80阅读模式
英文:

Makefile: not detecting that target executable has already been built (gnu make)

问题

介绍

我有一个在纯C中的简单项目,我正在使用手写的Makefile和gcc(在Linux中构建它)。

它工作得(几乎)完美,但有一个小问题:它不能检测到目标已经构建。

Makefile + 项目结构

它由一个单独的.c.h文件组成,编译成一个可执行文件。

comms_test    <-- 最终的可执行文件 
comms_test.c  
comms_test.h  
comms_test.o

我的Makefile看起来像这样:


PROJ := comms_test

CSRCS = $(PROJ).c

CC := cc
LD := gcc

INCLUDES := \
	    -I. 

LDFLAGS = 

CPPFLAGS = 

CFLAGS := \
	  -O0 \
	  -Wpedantic


OBJS += $(CSRCS:%.c=%.o)


.PHONY: all
all: options $(PROJ)

.PHONY: options 
options: 
	@echo $(PROJ) 构建选项:
	@echo "CC           = $(CC)"
	@echo "CFLAGS       = $(CFLAGS)"
	@echo "CPPFLAGS     = $(CPPFLAGS)"
	@echo "INCLUDES     = $(INCLUDES)"
	@echo "LDFLAGS      = $(LDFLAGS)"

%.o: %.c
	${CC} ${CPPFLAGS} ${CFLAGS} ${INCLUDES} -c $< -o $@

.PHONY: ${PROJ}
${PROJ}: ${OBJS}
	${LD} -o $@ ${OBJS} ${LDFLAGS}

.PHONY: clean
clean:
	rm -rf ${OBJS} ${PROJ}

.PHONY: echo
echo:
	@echo ${CSRCS}
	@echo ${OBJS}

.PHONY: test 

要构建,我只需调用make或(要跳过CC选项的打印)make comms_testmake clean等也可以正常工作。options目标是从其他项目保留下来的,我在其中抑制了在每个单独的.c文件上打印编译器标志,因此我只在顶部打印一次,以提高可读性。

问题

链接步骤不“理解”目标可执行文件已经构建。当我调用make comms_test时,它总是运行gcc -o comms_test comms_test.o,即使结果的可执行文件已经存在,并且输入的.o文件没有更改。

我如何修复这个问题?显然,我为此编写了错误的Make规则。有什么建议吗?

英文:

Introduction

I have a simple project in plain C, which I am building with a handwritten Makefile and gcc (in linux).

It works (almost) perfectly fine, but there is a small issue: It doesn't detect that the target has already been built.

Makefile + Project Structure

It consists of a single .c and .h file, which compiles to an executable.

comms_test    <-- resulting executable 
comms_test.c  
comms_test.h  
comms_test.o

My makefile looks like this:


PROJ := comms_test

CSRCS = $(PROJ).c

CC := cc
LD := gcc

INCLUDES := \
	    -I. 

LDFLAGS = 

CPPFLAGS = 

CFLAGS := \
	  -O0 \
	  -Wpedantic


OBJS += $(CSRCS:%.c=%.o)


.PHONY: all
all: options $(PROJ)

.PHONY: options 
options: 
	@echo $(PROJ) build options:
	@echo "CC           = $(CC)"
	@echo "CFLAGS       = $(CFLAGS)"
	@echo "CPPFLAGS     = $(CPPFLAGS)"
	@echo "INCLUDES     = $(INCLUDES)"
	@echo "LDFLAGS      = $(LDFLAGS)"

%.o: %.c
	${CC} ${CPPFLAGS} ${CFLAGS} ${INCLUDES} -c $< -o $@

.PHONY: ${PROJ}
${PROJ}: ${OBJS}
	${LD} -o $@ ${OBJS} ${LDFLAGS}

.PHONY: clean
clean:
	rm -rf ${OBJS} ${PROJ}

.PHONY: echo
echo:
	@echo ${CSRCS}
	@echo ${OBJS}

.PHONY: test 

To build, I just call make or (to skip the CC options printout) make comms_test. make clean etc pp works too just fine. The options target is a holdover from other projects where I suppress the printing of the compiler flags on each single .c file (might be dozens), so I just print them once at the top for better readability.

The Issue

The linking step doesn't "understand" that the target executable has already been built. When I call make comms_test, it always runs gcc -o comms_test comms_test.o, even though the resulting executable already exists, and the input .o file has not changed.

How can I fix this issue? I am clearly writing my make rule for that incorrectly. Any suggestion?


EDIT:

I just remembered, If I remove the .PHONY: ${PROJ} it won't relink the executable, but it still will do the options/all target. What I want, is to detect that the executable already exists, and then decide that the requirements for the options target are already satisfied. If nothing was actually compiled/linked, I also do NOT want the options output, but no output (or "nothing to be done"), otherwise this is a bit confusing.

答案1

得分: 1

标记一个目标为.PHONY指示(GNU)make在每次运行开始时将该目标视为过期,无论任何同名文件的存在或时间戳如何。正如在评论和您自己的回答中所讨论的那样,这就是您观察到程序comms_test被不必要重新链接(但不是comms_test.o被不必要重新编译)的原因。

这也是您在运行makemake all时看到options输出但在运行make comms_test时不看到的部分原因。options目标被标记为.PHONY(应该如此),并且它被指定为默认目标all的一个先决条件。因此,无论是明确构建all还是作为默认目标构建,都会运行options的配方。

我想要的是检测可执行文件已经存在,然后决定是否已满足options目标的要求。如果实际上没有编译/链接任何内容,我也不希望看到options输出,而是不带输出(或"nothing to be done"),否则会有点令人困惑。

您想要make构建的真正文件不应标记为.PHONY。不对应实际要构建的文件的目标应标记为.PHONY,但这只是为了在某些其他方式创建了相应文件的情况下防止行为不当。如果您希望仅在给定运行中构建了特定目标之前触发特定行为,则该行为应从该目标的配方中触发。如果将其作为目标的先决条件,那么这将导致每次运行都重新构建目标,因为虚假目标始终处于过时状态。

要从配方中触发行为,您需要控制配方,即提供自己的规则。您不能修改现有规则的配方,只能用不同的规则替换整个规则。

但还有另一个问题:规则的配方执行发生在所有规则的先决条件都被更新之后。如果要打印选项,则希望在构建任何内容之前对其进行评估/打印。在这种情况下,您可能可以通过直接从C源代码构建可执行文件来实现这一点,但由于您抱怨不必要的对象文件链接,这似乎不是您的实际情况。

options目标是从其他项目继承下来的,我在其中抑制了在每个单独的.c文件上打印编译器标志(可能有数十个),因此我只在顶部打印它们一次以提高可读性。

在一般情况下,您需要在所有贡献的.o文件的配方中控制这一点(除了在可执行文件的配方中)。通常情况下,这可以通过提供覆盖所有.o文件的隐式规则来完成,而不是为每个文件编写单独的规则。您还可以使用子make来为您执行条件逻辑。也许类似这样:

# ...

TS_FILE = build_ts

.PHONY: all

all:
	rm -f $(TS_FILE)
	$(MAKE) $(PROJ)

$(PROJ): $(OBJS)
	flock $(TS_FILE) $(MAKE) options
	$(CC) -o $@ $(CFLAGS) $(LDFLAGS) $^

%.o: %.c
	flock $(TS_FILE) $(MAKE) options
	$(CC) -o $@ $(CPPFLAGS) $(INCLUDES) $(CFLAGS) $<

# 不是.PHONY;产生一个实际文件
options: $(TS_FILE)
	@echo $(PROJ) build options: | tee $@
	@echo "CC           = $(CC)" | tee -a $@
	@echo "CFLAGS       = $(CFLAGS)"  | tee -a $@
	@echo "CPPFLAGS     = $(CPPFLAGS)"  | tee -a $@
	@echo "INCLUDES     = $(INCLUDES)"  | tee -a $@
	@echo "LDFLAGS      = $(LDFLAGS)"  | tee -a $@

解释:

  • all目标删除任何现有的时间戳文件,然后使用子make构建可执行文件。这会在执行任何其他操作之前删除时间戳文件,即使使用并行make

  • options目标是一个普通目标,产生一个选项的转录文件,并在输出中显示它们。因为它具有时间戳文件作为先决条件(而且不是.PHONY),所以只有在选项的转录文件不存在或比时间戳文件旧时,make才会运行配方。

  • 链接可执行文件的配方和构建所有.o文件的模式规则都使用子make来构建options目标。但这仍然存在并行make可能多次打印选项的可能性。为了防止这种情况发生,子make通过flock锁定时间戳文件,这将导致它被创建(如果尚不存在),并将所有make options执行序列化,因此只有第一个可以找到options已过期。

  • 不需要或不需要规则$(TS_FILE)

附加说明:

  • 如果您明确构建可执行文件或任何对象文件之一,那么可能会或可能不会打印选项,具体取决于之前构建的时间戳和选项文件是否仍然存在。

  • 如果您的文件系统具有低时间戳分辨率,则可能会在给定运行中多次看到选项被打印。

  • 总的来说,**

英文:

Marking a target .PHONY instructs (GNU) make to consider that target out of date at the beginning of every run, regardless of the existence or timestamp of any same-named file. As discussed in comments and your self-answer, that's why you are observing program comms_test being re-linked unnecessarily (but not comms_test.o being re-compiled unnecessarily).

That's also part of why you are seeing the options output whenever you make or make all but not when you make comms_test. The options target is marked .PHONY (as it should be), and it is designated a prerequisite of the the default target, all. Thus, the recipe for options will run whenever you build all, whether explicitly or as the default target.

> What I want, is to detect that the executable already exists, and then decide that the requirements for the options target are already satisfied. If nothing was actually compiled/linked, I also do NOT want the options output, but no output (or "nothing to be done"), otherwise this is a bit confusing.

A bona fide file that you want make to build should never be marked .PHONY. A target that does not correspond to an actual file to build should be marked .PHONY, but that just protects against misbehavior in the event that a corresponding file is created by some other means. If you have behavior that you want to be executed only in the event that a given target is built on a given run, before that target itself is built, then that behavior should be triggered from the recipe for that target. If you make it a prerequisite of the target then that will force the target to be rebuilt on every run, because the phony target is always initially out of date.

To trigger behavior from a recipe, you need to take control of the recipe -- that is, provide your own rule. You cannot amend the recipe of an existing rule, only replace the whole rule with a different one.

But there's another problem: behavior exercised by a rule's recipe happens after all the rule's prerequisites are brought up to date. You want the options to be evaluated / printed before anything is built if they are printed at all. You may be able to make that work in this case, by building the executable directly from the C sources, but inasmuch as you complain about unnecessary linking of object files, that does not seem to be your actual case.

> The options target is a holdover from other projects where I suppress the printing of the compiler flags on each single .c file (might be dozens), so I just print them once at the top for better readability.

In the general case, you do need to control this in the recipes for all the contributing .o files (in addition to in the recipe for the executable). Typically, however, this would be accomplished by providing your own implicit rule covering all the .o files, instead of writing a separate rule for every one. You can also use a sub-make to perform the conditional logic for you. Maybe something like this:

# ...

TS_FILE = build_ts

.PHONY: all

all:
	rm -f $(TS_FILE)
	$(MAKE) $(PROJ)

$(PROJ): $(OBJS)
	flock $(TS_FILE) $(MAKE) options
	$(CC) -o $@ $(CFLAGS) $(LDFLAGS) $^

%.o: %.c
	flock $(TS_FILE) $(MAKE) options
	$(CC) -o $@ $(CPPFLAGS) $(INCLUDES) $(CFLAGS) $&lt;

# Not .PHONY; produces an actual file
options: $(TS_FILE)
	@echo $(PROJ) build options: | tee $@
	@echo &quot;CC           = $(CC)&quot; | tee -a $@
	@echo &quot;CFLAGS       = $(CFLAGS)&quot;  | tee -a $@
	@echo &quot;CPPFLAGS     = $(CPPFLAGS)&quot;  | tee -a $@
	@echo &quot;INCLUDES     = $(INCLUDES)&quot;  | tee -a $@
	@echo &quot;LDFLAGS      = $(LDFLAGS)&quot;  | tee -a $@

Explanation:

  • The all target removes any pre-existing timestamp file, then uses a sub-make to build the executable. This gets the timestamp file removed before anything else is done, even with parallel make.

  • The options target is an ordinary target, producing a file a transcript of the options in addition to displaying them in the output. Because it has the timestamp file as a prerequisite (and that is not .PHONY either), make will run the recipe only if the options transcript does not already exist or is older than the timestamp file.

  • The recipe for linking the executable and the pattern rule by which all the .o files are built each use a sub-make to build the options target. But that leaves open the possibility that in a parallel make, the options would be printed more than once. To prevent that, the sub-make is gated by flocking the timestamp file, which both causes it to be created if it does not already exist, and serializes all the make options executions, so that only the first can find options out of date.

  • No rule for $(TS_FILE) is needed or wanted.

Additional notes:

  • If you build the executable or any of the object files explicitly then you might or might not get the options printed, depending on whether the timestamp and options files from a previous build are still present.

  • If your filesystem has low timestamp resolution then you might see the options printed more than once on a given run.

  • Overall, it would be much easier and more reliable to just print the options first on every run (or every build of the all target), regardless of whether anything else is to be done. You said that's not what you want, but it's what I would do in the unlikely event that I did anything at all of this sort.

答案2

得分: 1

你可以尝试类似以下的方式:

define OPTIONS
	@echo $(PROJ) 构建选项:
	@echo "CC           = $(CC)"
	@echo "CFLAGS       = $(CFLAGS)"
	@echo "CPPFLAGS     = $(CPPFLAGS)"
	@echo "INCLUDES     = $(INCLUDES)"
	@echo "LDFLAGS      = $(LDFLAGS)"
	$(eval OPTIONS=)
endef

define COMPILE
	$(OPTIONS)
	${CC} ${CPPFLAGS} ${CFLAGS} ${INCLUDES} -c $< -o $@
endef

define LINK
	$(OPTIONS)
	${LD} -o $@ ${OBJS} ${LDFLAGS}
endef

...

.PHONY: all
all: $(PROJ)

%.o: %.c
	$(COMPILE)

...

${PROJ}: ${OBJS}
    $(LINK)

第一次运行编译(COMPILE)或链接(LINK)时,选项将通过 OPTIONS 宏打印出来,并且 OPTIONS 宏将被重新定义为空字符串,以便在下一次编译/链接时不会打印任何内容。

英文:

You could try something like:

define OPTIONS
	@echo $(PROJ) build options:
	@echo &quot;CC           = $(CC)&quot;
	@echo &quot;CFLAGS       = $(CFLAGS)&quot;
	@echo &quot;CPPFLAGS     = $(CPPFLAGS)&quot;
	@echo &quot;INCLUDES     = $(INCLUDES)&quot;
	@echo &quot;LDFLAGS      = $(LDFLAGS)&quot;
	$(eval OPTIONS=)
endef

define COMPILE
	$(OPTIONS)
	${CC} ${CPPFLAGS} ${CFLAGS} ${INCLUDES} -c $&lt; -o $@
endef

define LINK
	$(OPTIONS)
	${LD} -o $@ ${OBJS} ${LDFLAGS}
endef

...

.PHONY: all
all: $(PROJ)

%.o: %.c
	$(COMPILE)

...

${PROJ}: ${OBJS}
    $(LINK)

The first time a compilation (COMPILE) or linking (LINK) is run the options are printed thanks to the OPTIONS macro and the OPTIONS macro is redefined as empty string, such that on the next compilation/linking nothing is printed.

答案3

得分: 0

感谢G.M.指导我找对了方向,那就是.PHONY ${PROJ}这一行。这并没有解决始终打印options输出的另一个问题,但这可能应该作为一个单独的问题提出。标记为已解决。

英文:

Thanks to G.M. for pointing me in the right direction, it was the .PHONY ${PROJ} line. That does not solve the other problem of it printing always the options output, but that should probably be posed as a separate question. Marking as solved

huangapple
  • 本文由 发表于 2023年7月17日 14:16:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/76701908.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定