Skip to content

Develop a C++ Application with Sibling Repositories

Sibling repositories keep an application and its libraries in separate Git repositories under one development directory. Each project keeps its own history and release cycle, while CMake builds the local checkouts together.

1
2
3
4
5
~/projects/
├── my_app/
│   └── .git/
└── logging_lib/
    └── .git/

This layout is useful when a library is shared by several applications or must be versioned independently. It also means every developer must check out the required repositories before configuring the application.

Minimal example

The example contains one application and one static library:

1
2
3
4
5
6
7
8
9
code/
├── sibling-projects.code-workspace
├── my_app/
│   ├── CMakeLists.txt
│   └── src/main.cpp
└── logging_lib/
    ├── CMakeLists.txt
    ├── include/logging/log.hpp
    └── src/log.cpp

In a real development directory, my_app and logging_lib would each be a separate Git repository. The example keeps them together only so they can be downloaded from this page.

The library

logging_lib/include/logging/log.hpp
1
2
3
4
5
6
7
8
9
#pragma once

#include <string_view>

namespace logging {

void log(std::string_view message);

}
logging_lib/src/log.cpp
#include "logging/log.hpp"

#include <iostream>

namespace logging {

void log(const std::string_view message)
{
    std::cout << "[log] " << message << '\n';
}

}
logging_lib/CMakeLists.txt
1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.16)
project(logging_lib LANGUAGES CXX)

add_library(logging_lib src/log.cpp)
add_library(logging_lib::logging_lib ALIAS logging_lib)

target_include_directories(logging_lib PUBLIC include)
target_compile_features(logging_lib PUBLIC cxx_std_17)

PUBLIC makes the library's include directory available to targets that link logging_lib::logging_lib.

The application

my_app/src/main.cpp
1
2
3
4
5
6
#include "logging/log.hpp"

int main()
{
    logging::log("Hello from my_app");
}
my_app/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(my_app LANGUAGES CXX)

set(LOGGING_LIB_SOURCE "" CACHE PATH "Path to the logging_lib source directory")
if(NOT EXISTS "${LOGGING_LIB_SOURCE}/CMakeLists.txt")
    message(FATAL_ERROR "Set LOGGING_LIB_SOURCE to the logging_lib source directory")
endif()

add_subdirectory("${LOGGING_LIB_SOURCE}" "${CMAKE_BINARY_DIR}/logging_lib")

add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE logging_lib::logging_lib)

LOGGING_LIB_SOURCE is a CMake cache path supplied by the developer. add_subdirectory() adds that source tree to the same build, and its second argument gives the library a build directory outside its source repository.

Build and run

From the application repository, configure CMake with the path to the sibling library:

1
2
3
4
5
cd ~/projects/my_app
cmake -S . -B build \
    -DLOGGING_LIB_SOURCE="$HOME/projects/logging_lib"
cmake --build build
./build/my_app

Expected output:

[log] Hello from my_app

The path may be relative too. From the bundled example, use:

1
2
3
4
cd docs/Programming/cpp/dev_env/project_struct/code/my_app
cmake -S . -B build -DLOGGING_LIB_SOURCE=../logging_lib
cmake --build build
./build/my_app

add_subdirectory() only combines the projects for this CMake build. It does not combine their Git histories: changes to the application and library are still committed in their respective repositories.


Use the sibling projects in VS Code

A multi-root workspace opens both repositories in one VS Code window. The workspace also passes the library path to the CMake Tools extension, so the graphical configure action is equivalent to the command-line example.

sibling-projects.code-workspace
{
    "folders": [
        {
            "name": "my_app",
            "path": "my_app"
        },
        {
            "name": "logging_lib",
            "path": "logging_lib"
        }
    ],
    "settings": {
        "cmake.sourceDirectory": "${workspaceFolder:my_app}",
        "cmake.buildDirectory": "${workspaceFolder:my_app}/build",
        "cmake.configureSettings": {
            "LOGGING_LIB_SOURCE": "${workspaceFolder:logging_lib}"
        }
    },
    "extensions": {
        "recommendations": [
            "ms-vscode.cpptools",
            "ms-vscode.cmake-tools"
        ]
    }
}

Place the workspace file beside the two repositories:

1
2
3
4
~/projects/
├── sibling-projects.code-workspace
├── my_app/
└── logging_lib/

Then open it:

cd ~/projects
code sibling-projects.code-workspace

Install the recommended extensions when VS Code prompts, then run these commands from the Command Palette:

  1. CMake: Select a Kit — choose the compiler.
  2. CMake: Configure — configure my_app and add logging_lib to its build.
  3. CMake: Build — build the application and library.
  4. CMake: Run Without Debugging — run my_app.

${workspaceFolder:logging_lib} resolves to the logging_lib folder named in the workspace file. This avoids storing a developer-specific absolute path in either Git repository.