Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a better way to structure a C project with cmake?

Tags:

c

cmake

I'm starting a new C project using CMake, so I created a directory structure very similar to the ones I use in Python (my "main" language). Although it compiles correctly, I'm not certain I'm doing it the right way. This is the current structure:

.
├── CMakeLists.txt
├── dist
│   └── # project will be built here, 'cmake ..'
├── extras
│   ├── CMakeLists.txt
│   ├── extra1
│   │   ├── CMakeLists.txt
│   │   ├── extra1.h
│   │   └── extra1.c
│   └── extra2
│       ├── CMakeLists.txt
│       ├── extra2.h
│       └── extra2.c
├── src
│   ├── CMakeLists.txt
│   ├── main.c
│   ├── module1.h
│   ├── module1.c
│   ├── module2.h
│   └── module2.c
└── test
    ├── CMakeLists.txt
    ├── test_module1.c
    └── test_module2.c

Since all files are distributed across multiple directories, I had to find a way to locate the libraries present in extras and the ones I need to test in src. So, these are my CMakeLists':

./CMakeLists.txt

cmake_minimum_required(VERSION 2.8)

project(MyProject)
add_definitions(-Wall -std=c99)

# I don't know really why I need this
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/dist)

add_subdirectory(src)
add_subdirectory(test)
add_subdirectory(extras)

enable_testing()

add_test(NAME DoTestModule1 COMMAND TestModule1)
add_test(NAME DoTestModule2 COMMAND TestModule2)

./src/CMakeLists.txt

macro(make_library target source)
    add_library(${target} ${source})
    target_include_directories(${target} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
endmacro(make_library) 

make_library(Module1.o module1.c)
make_library(Module2.o module2.c)

./test/CMakeLists.txt

macro(make_test target source library)
    add_executable(${target} ${source})
    target_link_libraries(${target} Libtap.o ${library})
    target_include_directories(${target} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
endmacro(make_test)

make_test(TestModule1 test_module1.c Module1.o)
make_test(TestModule2 test_module2.c Module2.o)

./extras/CMakeLists.txt

# Hopefully you'll never need to change this file
foreach(subdir ${SUBDIRS})
    add_subdirectory(${subdir})
endforeach()

(Finally) ./extras/libtap/CMakeLists.txt

add_library(Libtap.o tap.c)
target_include_directories(Libtap.o PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

So, now the question: the reason I'm worried is that this setup will create a 'public' library for every file I'm using, including extra libs (which are not meant to be distributed). If I have 10 libraries in src, 4 dependencies in extras (including libtap, which I'm using to test) and at least the same amount of test files, I'll end up with 24 compiled artifacts.

  • Is there any better way to expose libraries to linking?
  • I'm not compiling "main" yet, what would be the right configuration for that?
  • is add_definitions the right way to add flags to the compiler?
  • How can I make this structure more DRY?
like image 621
Lucas Sampaio Avatar asked Nov 11 '22 02:11

Lucas Sampaio


1 Answers

Is there any better way to expose libraries to linking?

No, this seems fine.

You might however want to reconsider the granularity at which you create static libraries. For example, if all applications except the tests will only ever use Module1 and Module2 in combination, you might want to merge them into a single library target. Sure, the tests will link against parts of the component that they do not use, but that is a small price to pay for the decrease in build complexity.

I'm not compiling "main" yet, what would be the right configuration for that?

There's nothing wrong with adding it to the src/CMakeLists.txt as well:

add_executable(my_main main.c)
target_link_libraries(my_main Module1.o Module2.o)

is add_definitions the right way to add flags to the compiler?

It can be used for that purpose, but might not be ideal.

Newer CMake scripts should prefer the target_compile_options command for this purpose. The only disadvantage here is that if you want to reuse the same compile options for all targets in your projects, you also have to do the same target_compile_options call for each of those. See below for tips on how to resolve that.

How can I make this structure more DRY?

First of all, unlike most program code, redundancy is often not that big an issue in build system code. The notable thing to look out for here is stuff that gets in the way of maintainability. Getting back to the common compiler options from before: Should you ever want to change those flags in the future, it is likely that you want to change them for every target. Here it makes sense to centralize the knowledge about the options: Either introduce a function at the top-level that sets the option for a given target, or store the options to a global variable.

In either case you will have to write one line per target to get the option, but it will not generate any maintenance overhead after that. As an added bonus, should you actually need to change the option for only one target in the future, you still have the flexibility to do so.

Still, take care not to overengineer things. A build system should first and foremost get things done.

If the easiest way to set it up means you copy/paste a lot, go for it! If during maintenance later it turns out that you have some real unnecessary redundancies, you can always refactor.

The sooner you accept the fact that your CMake scripts will never be as pretty as your program code, the better ;)

One small nitpick at the end: Avoid giving your target names extensions. That is, instead of

add_library(Libtap.o tap.c)

consider

add_library(Libtap tap.c)

CMake will automatically append the correct file ending depending on the target platform anyway.

like image 165
ComicSansMS Avatar answered Dec 16 '22 00:12

ComicSansMS