构建具有相似规则的多个可执行文件

Posted

技术标签:

【中文标题】构建具有相似规则的多个可执行文件【英文标题】:Building multiple executables with similar rules 【发布时间】:2011-10-30 16:58:26 【问题描述】:

我正在编写something,就像 C++ 的交互式教程一样。本教程将由两部分组成:一个被编译成一个库(我正在使用 Scons 来构建它),另一个(课程)随教程一起提供,由最终用户编译。我目前正在为人们寻找一种好的、简单的方法来构建这些课程。

基本上,第二部分是一个包含所有课程的目录,每个课程都在自己的目录中。每节课至少有一个lesson.cpp 和一个main.cpp 文件,可能还有其他文件,直到发布后我才知道它们的存在——最终用户将创建这些文件。它看起来像这样:

all_lessons/
    helloworld/
        lesson.cpp
        main.cpp
    even_or_odd/
        lesson.cpp
        main.cpp
    calculator/
        lesson.cpp
        main.cpp
        user_created_add.cpp

每一个都需要根据几乎相同的规则进行编译,并且编译命令应该可以从课程目录之一运行(helloworld/ 等)。

鉴于项目的其余部分是使用 Scons 构建的,因此在这部分中使用它也是有意义的。但是,Scons 在运行它的目录中搜索 SConstruct 文件:是否可以在每个课程目录中放置一个 SConstruct 文件,以及在给出一般规则的 all_lessons/ 目录中放置一个 SConscript ?这似乎与 Scons 期望项目组织的典型方式背道而驰:这种方法的潜在缺陷是什么?我可以放置一个 SConstruct 文件而不是 SConscript 文件,从而可以从任一目录构建(我猜是使用导出来避免无休止的递归)?

另外,我可能在某些时候想用生成必要文件的lesson.py 替换lesson.cpp; Scons 是否允许我与构建者一起轻松地完成此操作,或者是否有更方便的框架?

最后,我想得到以下结果(或等效于不同的构建系统):​​

all_lessons/
    SConstruct
    helloworld/
        SConstruct
        lesson.cpp
        main.cpp
    even_or_odd/
        SConstruct
        lesson.py
        main.cpp
    calculator/
        SConstruct
        lesson.cpp
        main.cpp
        user_created_add.cpp

all_lessons 目录中运行scons all 需要:

运行even_or_odd/lesson.py生成even_or_odd/lesson.cpp。 意识到user_created_add.cpp 也需要编译。 为每节课生成一个可执行文件。

even_or_odd/ 中运行scons 或在all_lessons/ 中运行scons even_or_odd 应生成与上述相同的可执行文件(相同的编译标志)。

总结:

    Scons 是否适合/能够做到这一点? 当SConscript 文件高于SConstruct 文件时,Scons 是否正常工作? Scons 是否可以很好地处理一个项目的多个 SConstrcut 文件,Sconscripting 彼此? Scons 构建器系统是否适合使用 Python 脚本生成 C++ 文件? 使用不同的构建系统/编写我自己的构建框架有什么好处吗?

当然,欢迎任何进一步的 cmets。

谢谢。

【问题讨论】:

看起来你正在寻找的是一个makefile。你是在 *nix 系统上吗? 我可以使用 Makefile,是的;使用 Scons 有什么好处? (我在 *nix 上,以前使用过 Makefile,但没有理由在更现代的构建系统上使用它们。) makefile 更好,因为它们体积小、易于制作且易于使用。 @Lockhead:makefile 很小,对于熟练使用 make 的人来说很容易制作。大多数构建系统可能也是如此。 【参考方案1】:

您实际上可以使用几行 GNU Make 来做到这一点。

下面是两个允许从all_lessons 目录和单个项目目录构建和清理的makefile。它假定该目录中的所有 C++ 源代码都包含一个以其目录命名的可执行文件。从***源目录 (all_lessons) 构建和清理时,它会构建和清理所有项目。从项目目录构建和清理时,它只构建和清理项目的二进制文件。

这些 makefile 还自动生成依赖项并且完全可并行化(make -j 友好)。

对于以下示例,我使用了与您相同的源文件结构:

$ find all_lessons
all_lessons
all_lessons/even_or_odd
all_lessons/even_or_odd/main.cpp
all_lessons/Makefile
all_lessons/helloworld
all_lessons/helloworld/lesson.cpp
all_lessons/helloworld/main.cpp
all_lessons/project.mk
all_lessons/calculator
all_lessons/calculator/lesson.cpp
all_lessons/calculator/user_created_add.cpp
all_lessons/calculator/main.cpp

为了能够从单个项目目录构建,project.mk 必须首先符号链接为 project/Makefile

[all_lessons]$ cd all_lessons/calculator/
[calculator]$ ln -s ../project.mk Makefile
[helloworld]$ cd ../helloworld/
[helloworld]$ ln -s ../project.mk Makefile
[even_or_odd]$ cd ../even_or_odd/
[even_or_odd]$ ln -s ../project.mk Makefile

让我们构建一个项目:

[even_or_odd]$ make
make -C .. project_dirs=even_or_odd all
make[1]: Entering directory `/home/max/src/all_lessons'
g++ -c -o even_or_odd/main.o -Wall -Wextra   -MD -MP -MF even_or_odd/main.d even_or_odd/main.cpp
g++ -o even_or_odd/even_or_odd even_or_odd/main.o  
make[1]: Leaving directory `/home/max/src/all_lessons'
[even_or_odd]$ ./even_or_odd
hello, even_or_odd

现在构建所有项目:

[even_or_odd]$ cd ..
[all_lessons]$ make
g++ -c -o calculator/lesson.o -Wall -Wextra   -MD -MP -MF calculator/lesson.d calculator/lesson.cpp
g++ -c -o calculator/user_created_add.o -Wall -Wextra   -MD -MP -MF calculator/user_created_add.d calculator/user_created_add.cpp
g++ -c -o calculator/main.o -Wall -Wextra   -MD -MP -MF calculator/main.d calculator/main.cpp
g++ -o calculator/calculator calculator/lesson.o calculator/user_created_add.o calculator/main.o  
g++ -c -o helloworld/lesson.o -Wall -Wextra   -MD -MP -MF helloworld/lesson.d helloworld/lesson.cpp
g++ -c -o helloworld/main.o -Wall -Wextra   -MD -MP -MF helloworld/main.d helloworld/main.cpp
g++ -o helloworld/helloworld helloworld/lesson.o helloworld/main.o  
[all_lessons]$ calculator/calculator 
hello, calculator
[all_lessons]$ helloworld/helloworld
hello, world

清理一个项目:

[all_lessons]$ cd helloworld/
[helloworld]$ make clean
make -C .. project_dirs=helloworld clean
make[1]: Entering directory `/home/max/src/all_lessons'
rm -f helloworld/lesson.o helloworld/main.o helloworld/main.d helloworld/lesson.d helloworld/helloworld
make[1]: Leaving directory `/home/max/src/all_lessons'

清理所有项目:

[helloworld]$ cd ..
[all_lessons]$ make clean
rm -f calculator/lesson.o calculator/user_created_add.o calculator/main.o even_or_odd/main.o helloworld/lesson.o helloworld/main.o calculator/user_created_add.d calculator/main.d calculator/lesson.d even_or_odd/main.d  calculator/calculator even_or_odd/even_or_odd helloworld/helloworld

makefile:

[all_lessons]$ cat project.mk 
all :
% : forward_ # build any target by forwarding to the main makefile
    $(MAKE) -C .. project_dirs=$(notdir $CURDIR) $@
.PHONY : forward_

[all_lessons]$ cat Makefile 
# one directory per project, one executable per directory
project_dirs := $(shell find * -maxdepth 0 -type d )

# executables are named after its directory and go into the same directory
exes := $(foreach dir,$project_dirs,$dir/$dir)

all : $exes

#  the rules

.SECONDEXPANSION:

objects = $(patsubst %.cpp,%.o,$(wildcard $(dir $1)*.cpp))

# link
$exes : % : $$(call objects,$$*) Makefile
    g++ -o $@ $(filter-out Makefile,$^) $LDFLAGS $LDLIBS

# compile .o and generate dependencies
%.o : %.cpp Makefile
    g++ -c -o $@ -Wall -Wextra $CPPFLAGS $CXXFLAGS -MD -MP -MF $@:.o=.d $<

.PHONY: clean

clean :
    rm -f $(foreach exe,$exes,$(call objects,$exe)) $(foreach dir,$project_dirs,$(wildcard $dir/*.d)) $exes

# include auto-generated dependency files
-include $(foreach dir,$project_dirs,$(wildcard $dir/*.d))

【讨论】:

哇,我不知道 make 可以做任何如此强大的事情,谢谢你的回答。 如果你想做任何类型的依赖跟踪,Makefiles 会变得很痛苦,例如当标题改变时重建东西。 Makefiles 没有任何“开箱即用”的功能。使用 SCons 即可。 @Nick:如果它是真的 makefile 将毫无用处,不是吗? gcc 使用-MD 选项生成完整的依赖关系,它需要 1 行 makefile 来包含依赖文件。【参考方案2】:

作为学习 scons 的练习,我试图回答您的问题。不幸的是,我不是专家,所以我不能告诉你什么是最好/理想的方法,但这里有一种可行的方法。

    Scons 适合/能够做到这一点。 (这正是构建工具的用途。) 不适用。 (我不知道。) Scons 似乎很好地工作一个项目的多个 SConstrcut 文件,SConscripting 彼此。 Scons 构建器系统可以使用 Python 脚本生成 C++ 文件。 不同的构建系统?各有所长。

使用您定义的层次结构,每个文件夹中都有一个 SConstruct 文件。您可以在子文件夹中运行scons 来构建该项目,或者在顶层运行以构建所有项目(不确定如何将“all”别名为默认构建)。您可以运行scons -c 来清理项目,scons 会自动找出它创建的文件并清理它们(包括生成的课程.cpp)。

但是,如果您希望编译器标志从***文件向下传播,我认为最好使用 SConscript 文件——除非我不确定是否要让这些文件自行编译。

./SConstruct

env = Environment()
env.SConscript(dirs=['calculator', 'even_or_odd', 'helloworld'], name='SConstruct')

./calculator/SConstruct 和 ./calculator/helloworld

env = Environment()
env.Program('program', Glob('*.cpp'))

./even_or_odd/SConstruct

env = Environment()

def add_compiler_builder(env):
    # filename transformation
    suffix = '.cpp'
    src_suffix = '.py'

    # define the build method
    rule = 'python $SOURCE $TARGET'

    bld = Builder(action = rule,
                  suffix = suffix,
                  src_suffix = src_suffix)
    env.Append(BUILDERS = 'Lesson' : bld)

    return env

add_compiler_builder(env)

env.Lesson('lesson.py')
env.Program('program', Glob('*.cpp'))

使用 SConscripts

我将子文件夹的 SConstructs 转换为 SConscripts,并且可以将代码构建细节从子文件夹中提取出来,但是您需要运行 scons -u 来构建子文件夹(向上搜索根 SConstruct)。

./SConstruct

def default_build(env):
    env.Program('program', Glob('*.cpp'))

env = Environment()
env.default_build = default_build

Export('env')
env.SConscript(dirs=['calculator', 'even_or_odd', 'helloworld'])

./helloworld/SConscript 等...

Import('env')
env.default_build(env)

【讨论】:

【参考方案3】:

编译命令必须从课程目录运行吗?如果没有,那么我会亲自创建 all_lessons/makefile,内容如下:

lessons = helloworld even_or_odd calculator

all: $(lessons)

# for each $lesson, the target is $lesson/main built from $lesson/main.cpp and $lesson/lesson.cpp
# NB: the leading space on the second line *must* be a tab character
$(lessons:%=%/main): %/main: %/main.cpp %/lesson.cpp
   g++ -W -Wall $+ -o $@

然后可以使用 all_lessons 目录中的“make”或“make all”来构建所有课程,或者使用例如“制作 helloworld/main”。

【讨论】:

是的,必须可以从课程目录调用构建命令。这个系统将如何处理新文件? 抱歉,如果我没有说清楚,但到那时我将无法修改makefile。我将编辑问题以澄清。 可以使用“make -f ../makefile”运行 make 以启用从子目录运行,尽管可能需要更改一些路径。只是一个想法 - 对于 C++ 教程,提供一些有关 makefile 的基本帮助可能会有所帮助,因为它们无处不在。或许可以让学生自己修改 makefile 以构建额外的课程。 我很确定任何有关 makefile 工作的帮助都超出了本课的范围;理想情况下,学生应该输入一个命令并让它“构建”。不幸的是,在纯 Scons 中执行此操作似乎过于复杂。当我有更多时间时,我会发布我设法弄清楚的内容。当然,仍然欢迎更多答案和/或 cmets。【参考方案4】:

据我所知,这是最好的解决方案:

该目录的结构相同,但课程没有多个 SConstruct 文件,而是每个课程都有一个 SConscript 文件,其中默认值会根据需要被覆盖。 SConstruct 文件由外部脚本根据需要生成,并调用 SCons。

概述:

all_lessons/
    helloworld/
        SConscript
        lesson.cpp
        main.cpp
    even_or_odd/
        SConscript
        lesson.py
        main.cpp
    calculator/
        SConscript
        lesson.cpp
        main.cpp
        user_created_add.cpp

使用GlobSConscript文件可以编译所有带有cpp扩展名的文件。它还可以使用生成课程的构建器(调用简单命令或成熟的命令),这意味着甚至可以将课程存储为元数据并在现场生成。

所以,回答问题:

    是的。 我不知道,但这不是为此目的所必需的。 据我所知,没有(除其他外,与 SConstruct 相关的路径存在问题)。 是的,有多种选择。 我不知道。

建议方法的缺点:这确实需要单独制作一个元构建系统。可以指定选项的文件数量较多,SConscript 文件有很大的出错空间。

【讨论】:

【参考方案5】:

这是我的方式。

# SConstruct or SConscript

def getSubdirs(dir) :   
    lst = [ name for name in os.listdir(dir) if os.path.isdir(os.path.join(dir, name)) and name[0] != '.' ]
    return lst

env = Environment()
path_to_lessons = '' # path to lessons
# configure your environment, set common rules and parameters for all lessons
for lesson in getSubdirs(path_to_lessons) :
    lessonEnv = env.Clone()
    # configure specific lesson, for example i'h ve checked SConscript file in lesson dir
    # and if it exist, execute it with lessonEnv and append env specific settings
    if File(os.path.join(path_to_lessons, lesson, 'lesson.scons')).exists() :
        SConscript(os.path.join(lesson, 'lesson.scons', export = ['lessonEnv'])

    # add lesson directory to include path
    lessonEnv.Append(CPPPATH = os.path.join(path_to_lessons, lesson));

    lessonEnv.Program(lesson, Glob(os.path.join(path_to_lessons, lesson, '*.cpp'))

现在你有:

env - 包含通用规则和参数的核心环境 所有课程 lessonEnv - 核心环境的克隆,但如果你有 course.scons 在特定的课程目录中,您可以额外配置 那个环境或者重写一些参数。

【讨论】:

以上是关于构建具有相似规则的多个可执行文件的主要内容,如果未能解决你的问题,请参考以下文章

我可以使用 JavaFX 原生构建工具拥有多个可执行文件吗?

CMake项目结构:如何正确地将库合并在一起并将它们包含在多个可执行文件中

从相同的代码库构建多个可执行文件

具有自动生成源的 Cmake 可执行文件

是否可以为 exec 平台构建部分可执行文件并将它们用作目标平台的工具链?

编写编译规则文件Makefile,并通过make生成可执行文件