【问题标题】:Compiled binary path based on compiler options基于编译器选项的编译二进制路径
【发布时间】:2019-03-31 08:59:27
【问题描述】:

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

我考虑过的一些选项:

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

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

有更好的方法/建议吗?

【问题讨论】:

  • 附带一个问题,您的测试如何适应更改后的名称?
  • 测试使用带有ifdefs的预处理器变量改变源码预编译

标签: c++ makefile dependencies


【解决方案1】:

有更好的方法/建议吗?

如果我理解正确,您的 GNU Make 构建系统可以构建多个 您的可执行文件,由定义(或未定义)的预处理器宏区分 在编译命令中取决于您的 Makefile 中测试的条件 和/或传递给make 的参数。你希望能够建立 这些变体中的任何一个都是独立的,不需要make clean 来删除 先前构建的工件,很可能是不同构建的构建 变种。

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

传统的解决方案是根据变体区分编译器所在的位置 将输出目标文件和相应的 place 链接器所在的位置 输出可执行文件。毫无疑问,您曾经使用过的所有 C/C++ IDE 都允许您 构建项目的 debug 变体或 release 变体,以及 他们将 debug 对象文件和可执行文件与 release 对象区分开来 通过在不同的子目录中生成文件和可执行文件 项目目录,例如

<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

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

在 makefile 中实现这个方案很简单。大多数 IDE 使用它的人在他们在幕后生成的makefile中实现它。 并且将方案扩展到更多变体也很简单,而不仅仅是 debugrelease (尽管你想要任何变体,你肯定会想要 debugrelease 这些变体的变体)。

这是我们想要在任何 我们将调用的两个构建属性组合得到的变体 TRAIT_ATRAIT_B:

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

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

我们的程序 prog 仅由一个源文件构建而成:

ma​​in.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 链接。 因此,纯粹出于演示的目的,我们将始终编译并链接该选项。 这是一个makefile,它采用一种区分所有变体的方法: 目标文件编译在./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'.

对于这个练习,您可以考虑改进 makefile 以让您构建或清理, 同时所有变体。或者,如果未定义 RELEASE,则默认为 DEBUG,反之亦然。或者如果没有选择有效的特征组合,则失败,对于 valid 的某些定义。

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

【讨论】:

  • 这是一个非常准确的答案,我非常感谢。我将使用这种方法来创建与我的选项相关的目录,并将目标文件和最终可执行文件都放在那里。
【解决方案2】:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多