基于编译器选项的编译二进制路径

Compiled binary path based on compiler options

本文关键字:编译 二进制 路径 选项 编译器      更新时间:2023-10-16

我有一个用C++编写的大型程序和库,并用make构建。在 makefile 中设置的大约十几个选项变成了更改实现的预处理器指令(使用ifdefs 等)。现在,我在运行代码之前更改这些编译器选项时make clean,从而暴力强制构建过程。我想设置系统,以便二进制文件的名称根据选项进行更改。但是,我担心将来会错过一个或添加一个,忘记更改名称等。有没有一种干净的方法来解决这个问题?

我考虑过的一些选项:

  1. 在构建时手动创建一个二进制名称,例如 APP.#{OPT_1}.#{OPT_2}.#{OPT_3}.#{OPT_4},然后运行该名称

  2. 从所有编译器标志(CXXFLAGS)创建一个哈希(例如,SHA1),并将该哈希放入我的二进制文件的名称中,例如APP.#{SHA1(CXXFLAGS)}。这在将来具有可扩展性的价值。

有什么更好的方法/建议吗?

有什么更好的方法/建议吗?

如果我理解正确,您的GNU Make构建系统可以构建几种变体 您的可执行文件,通过定义(或未定义)的预处理器宏进行区分 在编译命令中,具体取决于在生成文件中测试的条件 和/或您传递给make的参数。并且您希望能够构建 这些变体中的任何一个都独立存在,无需make clean即可删除 先前版本的工件,很可能是不同版本的产物 变体。

这是构建系统的基本需求之一。传统的解决方案不是 您正在考虑的一个 - 以某种方式将差异编码为 可执行文件。无论如何,这都行不通,除非您对 链接到可执行文件的目标文件。如果你不这样做,那么当 从变体 X 切换到变体Y,变体X对象文件foo.o不早于foo.cpp的,不需要重新编译, 即使应该用于变体Y,并且该变体Xfoo.o将被链接到 变体Y可执行文件,无论它叫什么。

传统的解决方案是按变体区分编译器的位置将输出目标文件,并相应地输出链接器的位置输出可执行文件。毫无疑问,您曾经使用过的所有C/C++ IDE都允许您 生成项目的调试变体或发布变体,以及 它们将调试对象文件和可执行文件与发布对象区分开来 文件和可执行文件,方法是在项目目录,例如

<projdir>/Debug/{obj|bin}
<projdir>/Release/{obj|bin}

或者也许:

<projdir>/obj/{debug|release}
<projdir>/bin/{debug|release}

此方法会自动对目标文件或可执行文件的变体进行编码 到其绝对路径名中,例如

<projdir>/Debug/obj/foo.o
<projdir>/bin/release/prog

事不宜迟,变体可以独立构建。

在生成文件中实现此方案很简单。大多数集成开发环境 使用它的人确实在幕后生成的生成文件中实现它。 而且,将方案扩展到更多变体也很简单,而不仅仅是调试和发布(尽管您想要什么变体,您肯定需要调试发布这些变体的变体)。

这是一个玩具程序的插图,我们想在任何 我们获得的两个构建属性组合的变体,我们将称之为TRAIT_ATRAIT_B

| TRAIT_A | TRAIT_B |
|---------|---------|
|    Y    |    Y    |
|---------|---------|
|    Y    |    N    |
|---------|---------|
|    N    |    Y    |
|---------|---------|
|    N    |    N    |

我们希望能够在调试模式或发布中构建任何这些变体。 模式。TRAIT_{A|B}可能直接映射到预处理器宏,或映射到 预处理器标志、编译器选项和/或链接选项的任意组合。

我们的程序prog仅从一个源文件构建:

主.cpp

#include <string>
#include <cstdlib>
int main(int atgc, char * argv[])
{
std::string cmd{"readelf -p .GCC.command.line "};
cmd += argv[0];
return system(cmd.c_str());
}

它所做的只是调用readelf来转储链接部分.GCC.command.line在其自己的可执行文件中。该链接部分仅在我们编译或 与 GCC 选项-frecord-gcc-switches链接。 因此,纯粹出于演示的目的,我们将始终编译并使用该选项进行链接。 下面是一个采用一种区分所有变体的方法的生成文件: 目标文件以./obj[/trait...]编译;可执行文件链接在./bin[/trait...]

生成文件

CXX = g++
CXXFLAGS := -frecord-gcc-switches
BINDIR := ./bin
OBJDIR := ./obj
ifdef RELEASE
ifdef DEBUG
$(error RELEASE and DEBUG are mutually exclusive)
endif
CPPFLAGS := -DNDEBUG
CXXFLAGS += -O3
BINDIR := $(BINDIR)/release
OBJDIR := $(OBJDIR)/release
endif
ifdef DEBUG
ifdef RELEASE
$(error RELEASE and DEBUG are mutually exclusive)
endif
CXXFLAGS += -O0 -g
BINDIR := $(BINDIR)/debug
OBJDIR := $(OBJDIR)/debug
endif
ifdef TRAIT_A
CPPFLAGS += -DTRAIT_A   # or whatever
BINDIR := $(BINDIR)/TRAIT_A
OBJDIR := $(OBJDIR)/TRAIT_A
endif
ifdef TRAIT_B
CPPFLAGS += -DTRAIT_B   # or whatever
BINDIR := $(BINDIR)/TRAIT_B
OBJDIR := $(OBJDIR)/TRAIT_B
endif
SRCS :=  main.cpp
OBJS := $(OBJDIR)/$(SRCS:.cpp=.o)
EXE := $(BINDIR)/prog
.PHONY: all clean
all: $(EXE)
$(EXE): $(OBJS) | $(BINDIR)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -o $@ $(LDFLAGS) $^ $(LIBS)
$(OBJDIR)/%.o: %.cpp | $(OBJDIR)
$(CXX) -c -o $@ $(CPPFLAGS) $(CXXFLAGS) $<
$(BINDIR) $(OBJDIR):
mkdir -p $@
clean:
$(RM) $(EXE) $(OBJS)

现在,让我们在调试模式下构建两个变体,在调试模式下构建另外两个变体 释放模式,一个接一个

$ make DEBUG=1 TRAIT_A=1
mkdir -p obj/debug/TRAIT_A
g++ -c -o obj/debug/TRAIT_A/main.o -DTRAIT_A     -frecord-gcc-switches -O0 -g main.cpp
mkdir -p bin/debug/TRAIT_A
g++ -DTRAIT_A    -frecord-gcc-switches -O0 -g -o bin/debug/TRAIT_A/prog  obj/debug/TRAIT_A/main.o
$ make DEBUG=1 TRAIT_B=1
mkdir -p obj/debug/TRAIT_B
g++ -c -o obj/debug/TRAIT_B/main.o -DTRAIT_B     -frecord-gcc-switches -O0 -g main.cpp
mkdir -p bin/debug/TRAIT_B
g++ -DTRAIT_B    -frecord-gcc-switches -O0 -g -o bin/debug/TRAIT_B/prog  obj/debug/TRAIT_B/main.o
$ make RELEASE=1 TRAIT_A=1 TRAIT_B=1
mkdir -p obj/release/TRAIT_A/TRAIT_B
g++ -c -o obj/release/TRAIT_A/TRAIT_B/main.o -DNDEBUG -DTRAIT_A      -DTRAIT_B   -frecord-gcc-switches -O3 main.cpp
mkdir -p bin/release/TRAIT_A/TRAIT_B
g++ -DNDEBUG -DTRAIT_A   -DTRAIT_B   -frecord-gcc-switches -O3 -o bin/release/TRAIT_A/TRAIT_B/prog  obj/release/TRAIT_A/TRAIT_B/main.o
$ make RELEASE=1
g++ -c -o obj/release/main.o -DNDEBUG -frecord-gcc-switches -O3 main.cpp
g++ -DNDEBUG -frecord-gcc-switches -O3 -o bin/release/prog  obj/release/main.o

最后一个是既没有TRAIT_A也没有TRAIT_B的发布变体。

我们现在已经在不同的./bin[/...]子目录中构建了四个版本的程序prog的项目,来自不同./obj[/...]子目录中的不同目标文件, 这些版本都将告诉我们它们是如何以不同的方式构建的。按顺序运行 我们建造了它们:-

$ bin/debug/TRAIT_A/prog
String dump of section '.GCC.command.line':
[     0]  -imultiarch x86_64-linux-gnu
[    1d]  -D_GNU_SOURCE
[    2b]  -D TRAIT_A
[    36]  main.cpp
[    3f]  -mtune=generic
[    4e]  -march=x86-64
[    5c]  -auxbase-strip obj/debug/TRAIT_A/main.o
[    84]  -g
[    87]  -O0
[    8b]  -frecord-gcc-switches
[    a1]  -fstack-protector-strong
[    ba]  -Wformat
[    c3]  -Wformat-security
$ bin/debug/TRAIT_B/prog
String dump of section '.GCC.command.line':
[     0]  -imultiarch x86_64-linux-gnu
[    1d]  -D_GNU_SOURCE
[    2b]  -D TRAIT_B
[    36]  main.cpp
[    3f]  -mtune=generic
[    4e]  -march=x86-64
[    5c]  -auxbase-strip obj/debug/TRAIT_B/main.o
[    84]  -g
[    87]  -O0
[    8b]  -frecord-gcc-switches
[    a1]  -fstack-protector-strong
[    ba]  -Wformat
[    c3]  -Wformat-security
$ bin/release/TRAIT_A/TRAIT_B/prog
String dump of section '.GCC.command.line':
[     0]  -imultiarch x86_64-linux-gnu
[    1d]  -D_GNU_SOURCE
[    2b]  -D NDEBUG
[    35]  -D TRAIT_A
[    40]  -D TRAIT_B
[    4b]  main.cpp
[    54]  -mtune=generic
[    63]  -march=x86-64
[    71]  -auxbase-strip obj/release/TRAIT_A/TRAIT_B/main.o
[    a3]  -O3
[    a7]  -frecord-gcc-switches
[    bd]  -fstack-protector-strong
[    d6]  -Wformat
[    df]  -Wformat-security
$ bin/release/prog
String dump of section '.GCC.command.line':
[     0]  -imultiarch x86_64-linux-gnu
[    1d]  -D_GNU_SOURCE
[    2b]  -D NDEBUG
[    35]  main.cpp
[    3e]  -mtune=generic
[    4d]  -march=x86-64
[    5b]  -auxbase-strip obj/release/main.o
[    7d]  -O3
[    81]  -frecord-gcc-switches
[    97]  -fstack-protector-strong
[    b0]  -Wformat
[    b9]  -Wformat-security

我们可以清理第一个:

$ make DEBUG=1 TRAIT_A=1 clean
rm -f ./bin/debug/TRAIT_A/prog ./obj/debug/TRAIT_A/main.o

最后一个:

$ make RELEASE=1 clean
rm -f ./bin/release/prog ./obj/release/main.o

第二个和第三个仍然存在并且是最新的:

$ make DEBUG=1 TRAIT_B=1
make: Nothing to be done for 'all'.
$ make RELEASE=1 TRAIT_A=1 TRAIT_B=1
make: Nothing to be done for 'all'.

对于本练习,您可以考虑优化生成文件,以便您构建或清理 所有变体同时进行。或者,如果未定义RELEASE则默认为DEBUG,反之亦然。或者,如果没有选择有效的特征组合,则失败,对于有效的某些定义。

顺便说一句,请注意,预处理器选项通常是在 make 变量中分配的CPPFLAGS,用于 C 或 C++ 编译;分配了 C编译器选项CFLAGS和C++编译器选项在CXXFLAGS.GNU Make的内置 规则假定您遵循这些约定。

我不确定仅使用二进制名称来分隔不同的构建配置是个好主意。编译器选项的更改是否仍然会导致目标文件被踩踏,因为我认为它们的名称将与源文件相同,并且无论如何它仍然会有效地完全重建?

在我看来,这是源外构建的主要候选者。配置生成脚本以在源目录之外的单独目录中创建所有中间文件和输出文件。每组不同的构建选项将使用不同的构建目录,可能会根据编译器标志的哈希动态选择目录名称,如您所建议的那样。

这将为您提供一个干净的源代码树,通过更改 makefile/build 脚本中的编译器选项,您将更改中间文件和输出文件将放置在哪个目录。