将 Make 选项转换为工具标志

作者:Mitch Frazier

在程序开发中,经常需要在多种配置中/为多种配置构建程序。很多时候,autoconf及其生成的configure脚本可以满足您的需求。其他时候,您只需更改#define在您的代码中。但有时,autoconf不是一个选项,并且更改 define 也不完全奏效(例如,您需要将您的 defines/undefines 传递给m4或一些其他无法处理包含文件的工具)。解决方案可能是更改您的 makefile。这里介绍的方法可以对您的 makefile 进行相当简洁的修改。

让我们从以下简单的 makefile 开始

# Makefile

all: a.out

a.out: test.o
    gcc test.o -o a.out

test.o: test.c
    gcc -c test.c

现在假设我们有 3 个不同的配置选项,我们希望能够在构建时启用它们:OPTION_A、OPTION_B 和 OPTION_C。因此,我们希望能够通过在make命令行

  $ make OPTION_B=1 OPTION_C=1
我们还希望能够在 makefile 中默认启用其中一些选项,然后在我们调用时禁用它们make:
  $ make OPTION_A=
在 makefile 内部,我们希望能够将选项标志用作变量
  ifdef OPTION_A
  ...
  endif
我们还希望为我们工具链中的所有选项设置标志
  $ make OPTION_A= OPTION_B=1 OPTION_C=1
  # ... will result in:
  gcc -UOPTION_A -DOPTION_B -DOPTION_C

修改后的 makefile 是
# Makefile

CONFIG_OPTIONS=\
    OPTION_A \
    -OPTION_B \
    -OPTION_C

SET_CONFIG_OPTIONS=$(filter-out -%,$(CONFIG_OPTIONS))
UNSET_CONFIG_OPTIONS=$(patsubst -%,%,$(filter -%,$(CONFIG_OPTIONS)))
ALL_CONFIG_OPTIONS=$(SET_CONFIG_OPTIONS) $(UNSET_CONFIG_OPTIONS)
#$(info Set: $(SET_CONFIG_OPTIONS))
#$(info Unset: $(UNSET_CONFIG_OPTIONS))

# Turn config options into make variables.
$(foreach cfg,$(SET_CONFIG_OPTIONS),$(eval $(cfg)=1))
$(foreach cfg,$(UNSET_CONFIG_OPTIONS),$(eval $(cfg)=))

# Make sure none of the options are set to anything except 1 or blank.
# Using "make OPTION=0" doesn't work, since "0" is set, you need "make OPTION=".
$(foreach cfg,$(ALL_CONFIG_OPTIONS), \
    $(if $(patsubst %1,%,$(value $(cfg))), \
        $(error Use "$(cfg)=1" OR "$(cfg)=" not "$(cfg)=$(value $(cfg))")))

# Turn them into tool flags (-D).
TOOL_DEFINES+=$(foreach cfg,$(ALL_CONFIG_OPTIONS),$(if $(value $(cfg)),-D$(cfg),-U$(cfg)))
#$(info $(TOOL_DEFINES))

# Configuration options are turned into make variables above.
ifdef OPTION_A
$(info Building with option-a)
endif
ifdef OPTION_B
$(info Building with option-b)
endif
ifdef OPTION_C
$(info Building with option-c)
endif

all: a.out

a.out: test.o
    gcc $(TOOL_DEFINES) test.o -o a.out

test.o: test.c
    gcc $(TOOL_DEFINES) -c test.c

允许的配置选项被添加到 makefile 开头的 make 变量中

  CONFIG_OPTIONS=\
          OPTION_A \
          -OPTION_B \
          -OPTION_C
以减号开头的选项名称默认禁用,不带减号的选项名称默认启用。

设置配置选项后,我们设置了三个变量SET_CONFIG_OPTIONS, UNSET_CONFIG_OPTIONS,以及ALL_CONFIG_OPTIONS它们分别包含已设置选项、未设置选项和所有选项的名称(所有选项都不带减号)

  SET_CONFIG_OPTIONS=$(filter-out -%,$(CONFIG_OPTIONS))
  UNSET_CONFIG_OPTIONS=$(patsubst -%,%,$(filter -%,$(CONFIG_OPTIONS)))
  ALL_CONFIG_OPTIONS=$(SET_CONFIG_OPTIONS) $(UNSET_CONFIG_OPTIONS)
对于已设置的选项,我们过滤掉所有以减号开头的单词。对于未设置的选项,我们“过滤”出所有以减号开头的单词,然后删除减号。所有选项只是已设置和未设置选项的组合。

接下来,我们将所有选项转换为 make 变量

  $(foreach cfg,$(SET_CONFIG_OPTIONS),$(eval $(cfg)=1))
  $(foreach cfg,$(UNSET_CONFIG_OPTIONS),$(eval $(cfg)=))
这只是循环遍历每个已设置/未设置的变量名称,然后将值设置为 “1”(使用eval)对于已设置的变量,对于未设置的变量则为空白。请记住,任何尝试设置在make命令行中指定的变量都会被make忽略,因此命令行上的值将覆盖 makefile 中的值。

之后,下一个过于复杂的 make 语句检查以确保所有配置变量的值为 “1” 或空白

  $(foreach cfg,$(ALL_CONFIG_OPTIONS), \
          $(if $(patsubst %1,%,$(value $(cfg))), \
                  $(error Use "$(cfg)=1" OR "$(cfg)=" not "$(cfg)=$(value $(cfg))")))
为此,我们循环遍历所有配置选项,iftest 执行模式替换,将 “%1” 更改为 “%”,以从变量的值中删除 “1”。如果在模式替换后还有任何内容,则配置变量具有无效值,并且我们打印错误信息。

最后,我们将所有配置选项转换为工具标志 defines 和 undefines

  TOOL_DEFINES+=$(foreach cfg,$(ALL_CONFIG_OPTIONS),$(if $(value $(cfg)),-D$(cfg),-U$(cfg)))
并修改我们的工具调用以包含这些标志
  a.out: test.o
          gcc $(TOOL_DEFINES) test.o -o a.out

  test.o: test.c
          gcc $(TOOL_DEFINES) -c test.c

这个示例程序test.c

#include <stdio.h>

main()
{
#ifdef OPTION_A
    printf("Hello option-a\n");
#endif
#ifdef OPTION_B
    printf("Hello option-b\n");
#endif
#ifdef OPTION_C
    printf("Hello option-c\n");
#endif
}

以下是来自不同构建的一些输出

  # Default is OPTION_A on:
  $ rm -f a.out test.o; make; ./a.out
  Building with option-a
  gcc -DOPTION_A -UOPTION_B -UOPTION_C -c test.c
  gcc -DOPTION_A -UOPTION_B -UOPTION_C test.o -o a.out
  Hello option-a

  # Turn off OPTION_A and turn on OPTION_B:
  $ rm -f a.out test.o; make OPTION_A= OPTION_B=1; ./a.out
  Building with option-b
  gcc -UOPTION_A -DOPTION_B -UOPTION_C -c test.c
  gcc -UOPTION_A -DOPTION_B -UOPTION_C test.o -o a.out
  Hello option-b

  # Turn off OPTION_A and turn on OPTION_B and OPTION_C:
  $ rm -f a.out test.o; make OPTION_A= OPTION_B=1 OPTION_C=1; ./a.out
  Building with option-b
  Building with option-c
  gcc -UOPTION_A -DOPTION_B -DOPTION_C -c test.c
  gcc -UOPTION_A -DOPTION_B -DOPTION_C test.o -o a.out
  Hello option-b
  Hello option-c

  # Turn off OPTION_A and OPTION_B and turn on OPTION_C:
  $ rm -f a.out test.o; make OPTION_A= OPTION_B= OPTION_C=1; ./a.out
  Building with option-c
  gcc -UOPTION_A -UOPTION_B -DOPTION_C -c test.c
  gcc -UOPTION_A -UOPTION_B -DOPTION_C test.o -o a.out
  Hello option-c

  # Try to get tricky:
  $ rm -f a.out test.o; make OPTION_C=on
  Makefile.mak:20: *** Use "OPTION_C=1" OR "OPTION_C=" not "OPTION_C=on".  Stop.

乍一看,人们可能会说,考虑到生成的 makefile 相对于原始文件的大小,这并不是一个特别“简洁”的解决方案。然而,原始的 makefile 非常简单,不太可能代表您在实际程序中实际使用的任何东西。最重要的是,如果原始 makefile 包含使用不同配置选项进行构建的能力,它会更长。此外,如果您删除注释、行继续符、信息打印和空行,则修改仅导致增加 8 行。您可能希望将CONFIG_OPTIONS=...行之后的行放在一个包含文件中,然后您可以重复使用它。

Mitch Frazier 是 Emerson Electric Co. 的嵌入式系统程序员。自 2000 年代初期以来,Mitch 一直是 Linux Journal 的贡献者和朋友。

加载 Disqus 评论