SCons and/or CMake: any way to automatically map from "header included during compilation" to "corresponding object file must be linked"?

StackOverflow https://stackoverflow.com/questions/18141648

  •  24-06-2022
  •  | 
  •  

Question

Super-simple, totally boring setup: I have a directory full of .hpp and .cpp files. Some of these .cpp files need to be built into executables; naturally, these .cpp files #include some of the .hpp files in the same directory, which may then include others, etc. etc. Most of those .hpp files have corresponding .cpp files, which is to say: if some_application.cpp #includes foo.hpp, either directly or transitively, then chances are there's also a foo.cpp file that needs to be compiled and linked into the some_application executable.

Super-simple, but I'm still clueless about what the "best" way to build it is, either in SCons or CMake (neither of which I have any expertise in yet, other than staring at documentation for the last day or so and becoming sad). I fear that the sort of solution I want may actually be impossible (or at least grossly overcomplicated) to pull off in most build systems, but if so, it'd be nice to know that so I can just give up and be less picky. Naturally, I'm hoping I'm wrong, which wouldn't be surprising given how ignorant I am about build systems (in general, and about CMake and SCons in particular).

CMake and SCons can, of course, both automatically detect that some_application.cpp needs to be recompiled whenever any of the header files it depends on (either directly or transitively) changes, since they can "parse" C++ files well enough to pick out those dependencies. OK, great: we don't have to list each .cpp-#includes-.hpp dependency by hand. But: we still need to decide what subset of object files need to get sent to the linker when it's time to actually generate each executable.

As I understand it, the two most straightforward alternatives to dealing with that part of the problem are:

  • A. Explicitly and laboriously enumerating the "anything using this object file needs to use these other object files too" dependencies by hand, even though those dependencies are exactly mirrored by the corresponding-.cpp-transitively-includes-the-corresponding-.hpp dependencies that the build system already went to the trouble of figuring out for us. Why? Because computers.
  • B. Dumping all the object files in this directory into a single "library", and then having all executables depend on and link in that one library. This is much simpler, and what I understand most people would do, but it's also kinda sloppy. Most of the executables don't actually need everything in that library, and wouldn't actually need to be rebuilt if only the contents of one or two .cpp files changed. Isn't this setting up exactly the kind of unnecessary computation a supposed "build system" should be avoiding? (I suppose maybe they wouldn't need to be rebuilt if the library were dynamically linked, but suffice it to say I dislike dynamically linked libraries for other reasons.)

Can either CMake or SCons do better than this in any remotely straightforward fashion? I see a bunch of limited ways to twiddle the automatically generated dependency graph, but no general-purpose way to do so interactively ("OK, build system, what do you think the dependencies are? Ah. Well, based on that, add the following dependencies and think again: ..."). I'm not too surprised about that. I haven't yet found a special-purpose mechanism in either build system for dealing with the super-common case where link-time dependencies should mirror corresponding compile-time #include dependencies, though. Did I miss something in my (admittedly somewhat cursory) reading of the documentation, or does everyone just go with option (B) and quietly hate themselves and/or their build systems?

Was it helpful?

Solution

Your statement in point A) "anything using this object file needs to use these other object files too" is something that will indeed need to be done by hand. Compilers dont automatically find object files needed by a binary. You have to explicitly list them at link time. If I understand your question correctly, you dont want to have to explicitly list the objects needed by a binary, but want the build tool to automatically find them. I doubt there is any build too that does this: SCons and Cmake definitely dont do this.

If you have an application some_application.cpp that includes foo.hpp (or other headers used by these cpp files), and subsequently needs to link the foo.cpp object, then in SCons, you will need to do something like this:

env = Environment()
env.Program(target = 'some_application',
            source = ['some_application.cpp', 'foo.cpp'])

This will only link when 'some_application.cpp', 'foo.hpp', or 'foo.cpp' have changed. Assuming g++, this will effectively translate to something like the following, independently of SCons or Cmake.

g++ -c foo.cpp -o foo.o
g++ some_application.cpp foo.o -o some_application

You mention you have "a directory full of .hpp and .cpp files", I would suggest you organize those files into libraries. Not all in one library, but logically organize them into smaller, cohesive libraries. Then your applications/binaries would link the libraries they need, thus minimizing recompilations due to not used objects.

OTHER TIPS

I had more or less the same problem as you have and I solved it as follows:

import SCons.Scanner
import os

def header_to_source(header_file):
    """Specify the location of the source file corresponding to a given
    header file."""
    return header_file.replace('include/', 'src/').replace('.hpp', '.cpp')

def source_files(main_file, env):
    """Returns list of source files the given main_file depends on.  With
    the function header_to_source one must specify where to look for
    the source file corresponding to a given header.  The resulting
    list is filtered for existing files.  The resulting list contains
    main_file as first element."""
    ## get the dependencies 
    node = File(main_file)
    scanner = SCons.Scanner.C.CScanner()
    path = SCons.Scanner.FindPathDirs("CPPPATH")(env)
    deps = node.get_implicit_deps(env, scanner, path)

    ## collect corresponding source files
    root_path = env.Dir('#').get_abspath()
    res = [main_file]
    for dep in deps:
        source_path = header_to_source(
            os.path.relpath(dep.get_abspath(), root_path))
        if os.path.exists(os.path.join(root_path, source_path)):
            res.append(source_path)

    return res

The header_to_source method is the one you need to modify such that it returns the source file corresponding to a given header file. Then the method source_file gives you all the source files you need to build the given main_file (including the main_file as first element). Non existing files are automatically removed. So the following should be sufficient to define the target for an executable:

env.Program(source_files('main.cpp', env))

I am not sure whether this works in all possible setups, but at least for me it works.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top