Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 29 additions & 15 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@
cmake_minimum_required(VERSION 3.25)

# Platform-specific compiler detection (must be before project())
if(APPLE)
# macOS: Use Homebrew LLVM if not specified
if(NOT DEFINED CMAKE_CXX_COMPILER)
set(CMAKE_CXX_COMPILER /opt/homebrew/opt/llvm/bin/clang++)
endif()
if(NOT DEFINED CMAKE_C_COMPILER)
set(CMAKE_C_COMPILER /opt/homebrew/opt/llvm/bin/clang)
endif()
elseif(UNIX)
# By default, use system compiler. Override with -DCMAKE_CXX_COMPILER=...
if(UNIX AND NOT APPLE)
# Linux: Prefer clang-18 if not specified
if(NOT DEFINED CMAKE_CXX_COMPILER)
find_program(CLANG_CXX NAMES clang++-18 clang++)
Expand All @@ -37,15 +30,17 @@ set(CMAKE_C_STANDARD_REQUIRED ON)

# Platform-specific flags
if(APPLE)
# macOS: Use Homebrew LLVM libc++
# macOS: Use system libc++ (works with both Apple Clang and MacPorts)
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-stdlib=libc++>)
add_link_options($<$<COMPILE_LANGUAGE:CXX>:-stdlib=libc++>)
add_link_options($<$<COMPILE_LANGUAGE:CXX>:-L/opt/homebrew/opt/llvm/lib/c++>)
add_link_options($<$<COMPILE_LANGUAGE:CXX>:-Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++>)

set(CMAKE_PREFIX_PATH "/opt/homebrew;/opt/homebrew/opt/abseil")
set(Protobuf_INCLUDE_DIR "/opt/homebrew/include")
set(Protobuf_PROTOC_EXECUTABLE "/opt/homebrew/bin/protoc")
# Support MacPorts (/opt/local) or Homebrew (/opt/homebrew)
if(EXISTS "/opt/local")
list(APPEND CMAKE_PREFIX_PATH "/opt/local")
endif()
if(EXISTS "/opt/homebrew")
list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew" "/opt/homebrew/opt/abseil")
endif()
elseif(UNIX)
# Linux: No special flags needed (use distribution's default stdlib)
endif()
Expand Down Expand Up @@ -80,6 +75,25 @@ else()
find_package(OpenSSL REQUIRED)
endif()

# fmt library for std::format compatibility on older platforms
# (macOS < 13.3 lacks to_chars for floating point required by std::format)
if(APPLE)
# Check if we need fmt (macOS < 13.3)
# Try to find fmt in MacPorts location
find_library(FMT_LIBRARY NAMES fmt PATHS /opt/local/lib/libfmt11 /opt/local/lib)
find_path(FMT_INCLUDE_DIR fmt/format.h PATHS /opt/local/include/libfmt11 /opt/local/include)
if(FMT_LIBRARY AND FMT_INCLUDE_DIR)
message(STATUS "Found fmt: ${FMT_LIBRARY}")
set(SPARKPLUG_USE_FMT ON CACHE BOOL "Use fmt library for format" FORCE)
add_compile_definitions(SPARKPLUG_USE_FMT)
# Make fmt available globally for examples and tests
include_directories(${FMT_INCLUDE_DIR})
link_libraries(${FMT_LIBRARY})
else()
message(STATUS "fmt library not found, using std::format (requires macOS 13.3+)")
endif()
endif()

add_subdirectory(proto)
target_compile_options(sparkplug_proto PRIVATE -w)
add_subdirectory(src)
Expand Down
38 changes: 20 additions & 18 deletions examples/tck_edge_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <iostream>
#include <thread>

#include <fmt/format.h>

namespace sparkplug::tck {

TCKEdgeNode::TCKEdgeNode(TCKEdgeNodeConfig config)
Expand Down Expand Up @@ -93,7 +95,7 @@ void TCKEdgeNode::run_session_establishment_test(const std::vector<std::string>&

try {
log("INFO",
std::format(
fmt::format(
"Starting SessionEstablishmentTest: host={}, group={}, node={}, devices={}",
host_id, group_id, edge_node_id, params.size() > 3 ? params[3] : "none"));

Expand All @@ -109,7 +111,7 @@ void TCKEdgeNode::run_session_establishment_test(const std::vector<std::string>&
log("INFO", "NBIRTH published with bdSeq and metrics");

if (!device_ids.empty()) {
log("INFO", std::format("Published DBIRTH for {} device(s)", device_ids.size()));
log("INFO", fmt::format("Published DBIRTH for {} device(s)", device_ids.size()));
}

} catch (const std::exception& e) {
Expand All @@ -136,7 +138,7 @@ void TCKEdgeNode::run_session_termination_test(const std::vector<std::string>& p

try {
log("INFO",
std::format(
fmt::format(
"Starting SessionTerminationTest: host={}, group={}, node={}, devices={}",
host_id, group_id, edge_node_id, params.size() > 3 ? params[3] : "none"));

Expand All @@ -155,10 +157,10 @@ void TCKEdgeNode::run_session_termination_test(const std::vector<std::string>& p
for (const auto& device_id : device_ids_) {
auto result = edge_node_->publish_device_death(device_id);
if (!result) {
log("WARN", std::format("Failed to publish DDEATH for {}: {}", device_id,
log("WARN", fmt::format("Failed to publish DDEATH for {}: {}", device_id,
result.error()));
} else {
log("INFO", std::format("DDEATH published for device: {}", device_id));
log("INFO", fmt::format("DDEATH published for device: {}", device_id));
}
}

Expand Down Expand Up @@ -203,7 +205,7 @@ void TCKEdgeNode::run_send_data_test(const std::vector<std::string>& params) {

try {
log("INFO",
std::format("Starting SendDataTest: host={}, group={}, node={}, devices={}",
fmt::format("Starting SendDataTest: host={}, group={}, node={}, devices={}",
host_id, group_id, edge_node_id,
params.size() > 3 ? params[3] : "none"));

Expand Down Expand Up @@ -234,11 +236,11 @@ void TCKEdgeNode::run_send_data_test(const std::vector<std::string>& params) {
return;
}

log("INFO", std::format("NDATA message {} published with aliases", i + 1));
log("INFO", fmt::format("NDATA message {} published with aliases", i + 1));
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

log("INFO", std::format("Successfully sent {} NDATA messages", num_ndata_messages));
log("INFO", fmt::format("Successfully sent {} NDATA messages", num_ndata_messages));

if (!device_ids.empty()) {
log("INFO", "Sending DDATA messages from devices");
Expand All @@ -254,18 +256,18 @@ void TCKEdgeNode::run_send_data_test(const std::vector<std::string>& params) {

auto ddata_result = edge_node_->publish_device_data(device_id, ddata);
if (!ddata_result) {
log("ERROR", std::format("Failed to publish DDATA for {}: {}", device_id,
log("ERROR", fmt::format("Failed to publish DDATA for {}: {}", device_id,
ddata_result.error()));
publish_result("OVERALL: FAIL");
return;
}

log("INFO", std::format("DDATA message {} published for device {} with aliases",
log("INFO", fmt::format("DDATA message {} published for device {} with aliases",
i + 1, device_id));
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

log("INFO", std::format("Successfully sent {} DDATA messages for device {}",
log("INFO", fmt::format("Successfully sent {} DDATA messages for device {}",
num_ddata_messages, device_id));
}
}
Expand Down Expand Up @@ -310,7 +312,7 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id,
return stdx::unexpected("Edge Node already exists");
}

log("INFO", std::format("Creating Edge Node group_id={}, edge_node_id={}, devices={}",
log("INFO", fmt::format("Creating Edge Node group_id={}, edge_node_id={}, devices={}",
group_id, edge_node_id, device_ids.size()));

current_group_id_ = group_id;
Expand All @@ -328,7 +330,7 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id,
}

edge_config.command_callback = [this](const Topic& topic, const auto& /*payload*/) {
log("INFO", std::format("Received command on topic: {}", topic.to_string()));
log("INFO", fmt::format("Received command on topic: {}", topic.to_string()));
};

edge_config.primary_host_id = host_id;
Expand All @@ -342,7 +344,7 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id,
}

if (!host_id.empty()) {
log("INFO", std::format("Waiting for primary host '{}' to be online", host_id));
log("INFO", fmt::format("Waiting for primary host '{}' to be online", host_id));
constexpr int max_wait_ms = 10000;
constexpr int poll_interval_ms = 100;
int waited_ms = 0;
Expand All @@ -359,7 +361,7 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id,

if (!edge_node_->is_primary_host_online()) {
return stdx::unexpected(
std::format("Timeout waiting for primary host '{}' to be online", host_id));
fmt::format("Timeout waiting for primary host '{}' to be online", host_id));
}
}

Expand All @@ -380,7 +382,7 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id,
log("INFO", "NBIRTH published successfully");

for (const auto& device_id : device_ids) {
log("INFO", std::format("Publishing DBIRTH for device: {}", device_id));
log("INFO", fmt::format("Publishing DBIRTH for device: {}", device_id));

PayloadBuilder dbirth;
auto device_timestamp = get_timestamp();
Expand All @@ -393,10 +395,10 @@ auto TCKEdgeNode::create_edge_node(const std::string& host_id,

auto device_result = edge_node_->publish_device_birth(device_id, dbirth);
if (!device_result) {
log("WARN", std::format("Failed to publish DBIRTH for {}: {}", device_id,
log("WARN", fmt::format("Failed to publish DBIRTH for {}: {}", device_id,
device_result.error()));
} else {
log("INFO", std::format("DBIRTH published for device: {}", device_id));
log("INFO", fmt::format("DBIRTH published for device: {}", device_id));
}
}

Expand Down
34 changes: 34 additions & 0 deletions include/sparkplug/detail/compat.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,37 @@ using tl::expected;
using tl::unexpected;
} // namespace sparkplug::stdx
#endif

// std::format compatibility
// Use fmt library on platforms where std::format is unavailable or broken
// (e.g., macOS < 13.3 lacks to_chars for floating point required by std::format)
#if defined(SPARKPLUG_USE_FMT) || \
(defined(__APPLE__) && __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ < 130300)
# include <fmt/format.h>
namespace sparkplug::stdx {
using fmt::format;
} // namespace sparkplug::stdx
#else
# include <format>
namespace sparkplug::stdx {
using std::format;
} // namespace sparkplug::stdx
#endif

// std::unreachable compatibility
#if __cpp_lib_unreachable >= 202202L
# include <utility>
namespace sparkplug::stdx {
using std::unreachable;
} // namespace sparkplug::stdx
#else
namespace sparkplug::stdx {
[[noreturn]] inline void unreachable() {
# if defined(__GNUC__) || defined(__clang__)
__builtin_unreachable();
# elif defined(_MSC_VER)
__assume(false);
# endif
}
} // namespace sparkplug::stdx
#endif
10 changes: 9 additions & 1 deletion scripts/format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ if [ -n "$CLANG_FORMAT" ]; then
:
elif command -v clang-format-18 &> /dev/null; then
CLANG_FORMAT=clang-format-18
elif command -v clang-format-mp-18 &> /dev/null; then
# MacPorts clang-18
CLANG_FORMAT=clang-format-mp-18
elif [ -f /opt/local/bin/clang-format-mp-18 ]; then
CLANG_FORMAT=/opt/local/bin/clang-format-mp-18
elif [ -f /opt/homebrew/opt/llvm@18/bin/clang-format ]; then
CLANG_FORMAT=/opt/homebrew/opt/llvm@18/bin/clang-format
elif command -v /opt/homebrew/bin/clang-format &> /dev/null; then
Expand All @@ -17,7 +22,10 @@ elif command -v clang-format &> /dev/null; then
CLANG_FORMAT=clang-format
else
echo "Error: clang-format not found!"
echo "Install it with: brew install clang-format (macOS) or apt install clang-format-18 (Linux)"
echo "Install it with:"
echo " macOS (MacPorts): sudo port install clang-18"
echo " macOS (Homebrew): brew install llvm@18"
echo " Linux (Ubuntu): apt install clang-format-18"
exit 1
fi

Expand Down
24 changes: 19 additions & 5 deletions scripts/tidy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,27 @@

set -e

# Detect clang-tidy binary
if [ -f /opt/homebrew/opt/llvm/bin/clang-tidy ]; then
CLANG_TIDY=/opt/homebrew/opt/llvm/bin/clang-tidy
# Detect clang-tidy binary (prefer clang-tidy-18 to match CI)
if [ -n "$CLANG_TIDY" ]; then
# Use environment variable if set
:
elif command -v clang-tidy-18 &> /dev/null; then
CLANG_TIDY=clang-tidy-18
elif command -v clang-tidy-mp-18 &> /dev/null; then
# MacPorts clang-18
CLANG_TIDY=clang-tidy-mp-18
elif [ -f /opt/local/bin/clang-tidy-mp-18 ]; then
CLANG_TIDY=/opt/local/bin/clang-tidy-mp-18
elif [ -f /opt/homebrew/opt/llvm/bin/clang-tidy ]; then
CLANG_TIDY=/opt/homebrew/opt/llvm/bin/clang-tidy
elif command -v clang-tidy &> /dev/null; then
CLANG_TIDY=clang-tidy
else
echo "Error: clang-tidy not found!"
echo "Install it with: brew install llvm (macOS) or apt install clang-tidy-18 (Linux)"
echo "Install it with:"
echo " macOS (MacPorts): sudo port install clang-18"
echo " macOS (Homebrew): brew install llvm"
echo " Linux (Ubuntu): apt install clang-tidy-18"
exit 1
fi

Expand Down Expand Up @@ -64,7 +75,10 @@ done
# Find all C++ source files if none specified
# Only check .cpp files - headers will be analyzed when included
if [ ${#FILES_TO_CHECK[@]} -eq 0 ]; then
mapfile -t FILES_TO_CHECK < <(find src tests examples \
# Use while loop for portability (mapfile requires bash 4+)
while IFS= read -r file; do
FILES_TO_CHECK+=("$file")
done < <(find src tests examples \
-name "*.cpp" \
-not -path "*/build/*" \
-not -path "*/build-*/*")
Expand Down
6 changes: 6 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ target_include_directories(sparkplug_cpp
$<INSTALL_INTERFACE:include>
)

# Add fmt include directory and link library if using fmt fallback
if(SPARKPLUG_USE_FMT)
target_include_directories(sparkplug_cpp PUBLIC ${FMT_INCLUDE_DIR})
target_link_libraries(sparkplug_cpp PRIVATE ${FMT_LIBRARY})
endif()

if(BUILD_STATIC_BUNDLE)
target_link_libraries(sparkplug_cpp
PUBLIC
Expand Down
14 changes: 8 additions & 6 deletions src/c_bindings.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#include "sparkplug/detail/compat.hpp"
#include "sparkplug/edge_node.hpp"
#include "sparkplug/host_application.hpp"
#include "sparkplug/payload_builder.hpp"
#include "sparkplug/sparkplug_c.h"

#include <cstring>
#include <format>
#include <memory>

struct sparkplug_publisher {
Expand Down Expand Up @@ -220,7 +220,8 @@ int sparkplug_publisher_connect(sparkplug_publisher_t* pub) {

auto result = pub->impl.connect();
if (!result.has_value()) {
auto error_msg = std::format("EdgeNode MQTT connection failed: {}", result.error());
auto error_msg =
sparkplug::stdx::format("EdgeNode MQTT connection failed: {}", result.error());
pub->impl.log(sparkplug::LogLevel::ERROR, error_msg);
return -1;
}
Expand Down Expand Up @@ -894,8 +895,8 @@ int sparkplug_host_application_connect(sparkplug_host_application_t* host) {

auto result = host->impl.connect();
if (!result.has_value()) {
auto error_msg =
std::format("HostApplication MQTT connection failed: {}", result.error());
auto error_msg = sparkplug::stdx::format("HostApplication MQTT connection failed: {}",
result.error());
host->impl.log(sparkplug::LogLevel::ERROR, error_msg);
return -1;
}
Expand Down Expand Up @@ -951,8 +952,9 @@ int sparkplug_host_application_publish_node_command(sparkplug_host_application_t

auto result = host->impl.publish_node_command(group_id, target_edge_node_id, builder);
if (!result.has_value()) {
auto error_msg = std::format("publish_node_command failed for {}/{}: {}", group_id,
target_edge_node_id, result.error());
auto error_msg =
sparkplug::stdx::format("publish_node_command failed for {}/{}: {}", group_id,
target_edge_node_id, result.error());
host->impl.log(sparkplug::LogLevel::ERROR, error_msg);
}
return result.has_value() ? 0 : -1;
Expand Down
Loading
Loading