diff --git a/pj_base/CMakeLists.txt b/pj_base/CMakeLists.txt index c1328696..3d552d69 100644 --- a/pj_base/CMakeLists.txt +++ b/pj_base/CMakeLists.txt @@ -53,9 +53,11 @@ target_compile_options(pj_base PRIVATE ) # Header-only implementation dependencies stay off installed targets so # downstream consumers inherit neither their include paths nor link requirements. +# Header-only fmt is deliberately NOT linked here: even header-only, its +# implementation symbols land in the installed static archive and collide +# with a consumer's own fmt under MSVC (see test_sdk_install.sh guard). target_link_libraries(pj_base PRIVATE $ - $ ) set_target_properties(pj_base PROPERTIES POSITION_INDEPENDENT_CODE ON diff --git a/pj_base/src/time_format.cpp b/pj_base/src/time_format.cpp index 2d8194df..d1dc26ba 100644 --- a/pj_base/src/time_format.cpp +++ b/pj_base/src/time_format.cpp @@ -3,10 +3,9 @@ #include "pj_base/time_format.hpp" -#include - #include #include +#include #include "pj_base/time_math.hpp" @@ -38,13 +37,23 @@ UtcTime utcFromNanoseconds(int64_t ts_ns) { static_cast(time.seconds().count())}; } +// Plain snprintf, deliberately: linking fmt here would embed fmt +// implementation symbols in the installed pj_base archive, which collide +// (LNK2005) with any consumer that links its own fmt on MSVC. std::string formatDate(const UtcTime& utc, char separator, bool day_first) { - return day_first ? fmt::format("{:02}{}{:02}{}{:04}", utc.day, separator, utc.month, separator, utc.year) - : fmt::format("{:04}{}{:02}{}{:02}", utc.year, separator, utc.month, separator, utc.day); + char buffer[32]; + if (day_first) { + std::snprintf(buffer, sizeof buffer, "%02d%c%02d%c%04d", utc.day, separator, utc.month, separator, utc.year); + } else { + std::snprintf(buffer, sizeof buffer, "%04d%c%02d%c%02d", utc.year, separator, utc.month, separator, utc.day); + } + return buffer; } std::string formatTime(const UtcTime& utc) { - return fmt::format("{:02}:{:02}:{:02}", utc.hour, utc.minute, utc.second); + char buffer[16]; + std::snprintf(buffer, sizeof buffer, "%02d:%02d:%02d", utc.hour, utc.minute, utc.second); + return buffer; } bool isDigit(char character) { @@ -70,7 +79,12 @@ bool parseFixedDigits(std::string_view text, std::size_t offset, std::size_t cou std::string formatTimestamp(int64_t ts_ns, bool long_format) { const auto utc = utcFromNanoseconds(ts_ns); - return long_format ? fmt::format("{:02}/{:02} {}", utc.day, utc.month, formatTime(utc)) : formatTime(utc); + if (!long_format) { + return formatTime(utc); + } + char day_month[16]; + std::snprintf(day_month, sizeof day_month, "%02d/%02d ", utc.day, utc.month); + return day_month + formatTime(utc); } std::string formatDuration(int64_t duration_ns) { diff --git a/test_sdk_install.sh b/test_sdk_install.sh index a373bbbe..cc50eb04 100755 --- a/test_sdk_install.sh +++ b/test_sdk_install.sh @@ -123,5 +123,22 @@ if [[ $LEAKED -ne 0 ]]; then exit 1 fi +# --------------------------------------------------------------------------- +# 6. Verify pj_base ships no fmt implementation symbols +# --------------------------------------------------------------------------- + +echo "" +echo "--- Step 6: Verify libpj_base.a defines no fmt symbols ---" + +# fmt is a private, header-only implementation detail. If its implementation +# symbols land in the installed archive (weak on ELF, so Linux links fine), +# the MSVC build of the same code collides (LNK2005) with any consumer that +# links its own fmt — this check makes the Windows failure visible on Linux. +if nm -C "$STAGING_DIR/lib/libpj_base.a" | grep -E " [TWuVvW] " | grep -q "fmt::"; then + echo "ERROR: installed libpj_base.a defines fmt symbols (collides with a consumer's fmt on MSVC):" + nm -C "$STAGING_DIR/lib/libpj_base.a" | grep -E " [TWuVvW] " | grep "fmt::" | head -10 + exit 1 +fi + echo "" echo "=== plotjuggler_sdk install test PASSED ==="