Skip to content

Commit cbede0a

Browse files
bsergeanclaude
andauthored
Remove every CMake FetchContent dependency (#605)
CI has been failing because FetchContent clones break: the pinned cpp-linenoise commit is no longer reachable, and the windows_gcc job dies at configure time with "fatal: unable to read tree 67be78f3...". Rather than repin, drop the four fetches entirely so that cmake configures offline. spdlog -> common/IXLog.h, a small header only logger. It keeps the {} placeholder syntax so the ws call sites are unchanged, is mutex guarded, and can be redirected to a file (--logfile). No colored output anymore. msgpack11 -> common/IXPdu.h, a line based "<name> <hex value>" format used by ws send/receive/transfer and ws chat. Values are hex encoded so binary payloads need no escaping. This is not compatible with the msgpack based format used up to 12.0.1. cpp-linenoise -> plain std::getline in ws connect, like ws chat and ws ping already did. No history, no line editing. Catch2 v3.15.1 -> third_party/catch_amalgamated.{hpp,cpp} v3.15.3 CLI11 v2.6.2 -> third_party/CLI11.hpp, same version, so the ws command line is unchanged. Catch2 v3 needs C++14, which the fetched target used to require implicitly, so the vendored Catch2 target carries cxx_std_14 as a usage requirement. The library itself stays C++11. Vendored headers are included as system headers, third_party has a .clang-format that disables formatting, and the Catch2 target disables unity builds since those otherwise collide on main(). Also fix ws send, which never transferred a file intact: load() read files with a std::istream_iterator<char>, which skips whitespace bytes, so a 2MB random file was sent as 1999903 bytes. The djb2 check did not catch it because the sender hashed its own truncated read. Read the file with an unformatted binary read, and write output files in binary mode. Drop git from the alpine docker image (it was only needed by FetchContent) and delete the now unused CMake/FindSpdLog.cmake. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b02063e commit cbede0a

41 files changed

Lines changed: 40056 additions & 473 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CMake/FindSpdLog.cmake‎

Lines changed: 0 additions & 19 deletions
This file was deleted.

‎CMakeLists.txt‎

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -313,38 +313,19 @@ if (IXWEBSOCKET_INSTALL)
313313
)
314314
endif()
315315

316-
if (USE_WS OR USE_TEST)
317-
include(FetchContent)
318-
319-
set(MSGPACK11_BUILD_TESTS OFF CACHE BOOL "" FORCE)
320-
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
321-
322-
FetchContent_Declare(msgpack11
323-
GIT_REPOSITORY "https://github.com/ar90n/msgpack11"
324-
GIT_TAG "892264112b52b75b37cb1f83c41abfb5aff765af"
325-
GIT_SHALLOW 1)
326-
327-
FetchContent_Declare(spdlog
328-
GIT_REPOSITORY "https://github.com/gabime/spdlog"
329-
GIT_TAG "v1.17.0"
330-
GIT_SHALLOW 1)
331-
332-
FetchContent_MakeAvailable(spdlog msgpack11)
333-
334-
if (USE_WS)
335-
add_subdirectory(ws)
336-
endif()
337-
if (USE_TEST)
338-
FetchContent_Declare(Catch2
339-
GIT_REPOSITORY "https://github.com/catchorg/Catch2"
340-
GIT_TAG "v3.15.1"
341-
GIT_SHALLOW 1)
342-
343-
FetchContent_MakeAvailable(Catch2)
316+
#
317+
# ws and the unittest have no external dependency: everything they need is
318+
# either vendored in third_party (CLI11 and Catch2, as single files) or lives
319+
# in common/ (a tiny logger and serialization format). Nothing is downloaded at
320+
# configure time.
321+
#
322+
if (USE_WS)
323+
add_subdirectory(ws)
324+
endif()
344325

345-
enable_testing()
346-
add_subdirectory(test)
347-
endif()
326+
if (USE_TEST)
327+
enable_testing()
328+
add_subdirectory(test)
348329
endif()
349330

350331
if (BUILD_DEMO)

‎common/IXLog.h‎

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* IXLog.h
3+
* Author: Benjamin Sergeant
4+
* Copyright (c) 2026 Machine Zone, Inc. All rights reserved.
5+
*
6+
* A minimal, dependency free logger used by the ws utility and by the
7+
* unittest. It supports the {} placeholder syntax, writing to stdout/stderr
8+
* or to a log file, and is safe to call from multiple threads.
9+
*/
10+
11+
#pragma once
12+
13+
#include <chrono>
14+
#include <ctime>
15+
#include <fstream>
16+
#include <iostream>
17+
#include <mutex>
18+
#include <sstream>
19+
#include <string>
20+
21+
namespace ix
22+
{
23+
namespace logger
24+
{
25+
enum class Level
26+
{
27+
Debug = 0,
28+
Info = 1,
29+
Warn = 2,
30+
Error = 3
31+
};
32+
33+
namespace detail
34+
{
35+
struct State
36+
{
37+
std::mutex mutex;
38+
Level level = Level::Debug;
39+
std::ofstream file;
40+
};
41+
42+
inline State& state()
43+
{
44+
static State state;
45+
return state;
46+
}
47+
48+
// No argument left, the rest of the format string is written as is.
49+
inline void format(std::ostream& os, const char* fmt)
50+
{
51+
os << fmt;
52+
}
53+
54+
template<typename T, typename... Args>
55+
void format(std::ostream& os, const char* fmt, const T& value, const Args&... args)
56+
{
57+
for (; *fmt != '\0'; ++fmt)
58+
{
59+
if (fmt[0] == '{' && fmt[1] == '}')
60+
{
61+
os << value;
62+
format(os, fmt + 2, args...);
63+
return;
64+
}
65+
66+
os << *fmt;
67+
}
68+
}
69+
70+
inline const char* levelName(Level level)
71+
{
72+
switch (level)
73+
{
74+
case Level::Debug: return "debug";
75+
case Level::Warn: return "warning";
76+
case Level::Error: return "error";
77+
case Level::Info:
78+
default: return "info";
79+
}
80+
}
81+
82+
inline std::string timestamp()
83+
{
84+
auto now = std::chrono::system_clock::now();
85+
auto time = std::chrono::system_clock::to_time_t(now);
86+
87+
// std::localtime is the only portable option (localtime_r and
88+
// localtime_s are not available everywhere). It returns a
89+
// pointer to a shared buffer, which is fine here since all our
90+
// callers hold the logger mutex.
91+
#ifdef _MSC_VER
92+
#pragma warning(push)
93+
#pragma warning(disable : 4996)
94+
#endif
95+
struct tm tm = *std::localtime(&time);
96+
#ifdef _MSC_VER
97+
#pragma warning(pop)
98+
#endif
99+
auto ms =
100+
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) %
101+
1000;
102+
103+
char buffer[32];
104+
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm);
105+
106+
std::stringstream ss;
107+
ss << buffer << '.';
108+
ss.fill('0');
109+
ss.width(3);
110+
ss << ms.count();
111+
return ss.str();
112+
}
113+
114+
template<typename... Args>
115+
void write(Level level, const std::string& fmt, const Args&... args)
116+
{
117+
auto& s = state();
118+
std::lock_guard<std::mutex> lock(s.mutex);
119+
120+
if (level < s.level) return;
121+
122+
std::stringstream ss;
123+
format(ss, fmt.c_str(), args...);
124+
125+
std::ostream& os = s.file.is_open()
126+
? static_cast<std::ostream&>(s.file)
127+
: (level == Level::Error ? std::cerr : std::cout);
128+
129+
os << '[' << timestamp() << "] [" << levelName(level) << "] " << ss.str()
130+
<< std::endl;
131+
}
132+
} // namespace detail
133+
134+
inline void setLevel(Level level)
135+
{
136+
auto& s = detail::state();
137+
std::lock_guard<std::mutex> lock(s.mutex);
138+
s.level = level;
139+
}
140+
141+
// Redirect all logs to a file. Returns false if the file cannot be opened.
142+
inline bool setLogFile(const std::string& path)
143+
{
144+
auto& s = detail::state();
145+
std::lock_guard<std::mutex> lock(s.mutex);
146+
147+
s.file.open(path.c_str(), std::ios::out | std::ios::trunc);
148+
return s.file.is_open();
149+
}
150+
} // namespace logger
151+
152+
template<typename... Args>
153+
void logDebug(const std::string& fmt, const Args&... args)
154+
{
155+
logger::detail::write(logger::Level::Debug, fmt, args...);
156+
}
157+
158+
template<typename... Args>
159+
void logInfo(const std::string& fmt, const Args&... args)
160+
{
161+
logger::detail::write(logger::Level::Info, fmt, args...);
162+
}
163+
164+
template<typename... Args>
165+
void logWarn(const std::string& fmt, const Args&... args)
166+
{
167+
logger::detail::write(logger::Level::Warn, fmt, args...);
168+
}
169+
170+
template<typename... Args>
171+
void logError(const std::string& fmt, const Args&... args)
172+
{
173+
logger::detail::write(logger::Level::Error, fmt, args...);
174+
}
175+
} // namespace ix

‎common/IXPdu.h‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* IXPdu.h
3+
* Author: Benjamin Sergeant
4+
* Copyright (c) 2026 Machine Zone, Inc. All rights reserved.
5+
*
6+
* A minimal, dependency free serialization format, used by the ws utility to
7+
* exchange structured messages (the chat, send, receive and transfer modes).
8+
*
9+
* A pdu is a set of named fields, each one written on its own line as:
10+
*
11+
* <name> <hex encoded value>
12+
*
13+
* Names are plain ascii identifiers, values are hex encoded so that binary
14+
* payloads (a file content) can be carried without any escaping. This is not
15+
* compact (values take twice their size on the wire) but it is trivial to
16+
* produce, to parse and to eyeball while debugging.
17+
*
18+
* This format is not compatible with the msgpack based one used by
19+
* IXWebSocket 12.0.1 and earlier.
20+
*/
21+
22+
#pragma once
23+
24+
#include <map>
25+
#include <string>
26+
#include <vector>
27+
28+
namespace ix
29+
{
30+
typedef std::map<std::string, std::string> Pdu;
31+
32+
inline std::string hexEncode(const std::string& value)
33+
{
34+
static const char* digits = "0123456789abcdef";
35+
36+
std::string out;
37+
out.reserve(value.size() * 2);
38+
39+
for (auto&& c : value)
40+
{
41+
auto byte = static_cast<unsigned char>(c);
42+
out += digits[byte >> 4];
43+
out += digits[byte & 0x0f];
44+
}
45+
46+
return out;
47+
}
48+
49+
inline std::string hexEncode(const std::vector<uint8_t>& value)
50+
{
51+
return hexEncode(std::string(value.begin(), value.end()));
52+
}
53+
54+
inline bool hexDecode(const std::string& value, std::string& out)
55+
{
56+
if (value.size() % 2 != 0) return false;
57+
58+
out.clear();
59+
out.reserve(value.size() / 2);
60+
61+
int nibbles[2];
62+
for (size_t i = 0; i < value.size(); i += 2)
63+
{
64+
for (size_t j = 0; j < 2; ++j)
65+
{
66+
char c = value[i + j];
67+
68+
if (c >= '0' && c <= '9')
69+
{
70+
nibbles[j] = c - '0';
71+
}
72+
else if (c >= 'a' && c <= 'f')
73+
{
74+
nibbles[j] = c - 'a' + 10;
75+
}
76+
else if (c >= 'A' && c <= 'F')
77+
{
78+
nibbles[j] = c - 'A' + 10;
79+
}
80+
else
81+
{
82+
return false;
83+
}
84+
}
85+
86+
out += static_cast<char>((nibbles[0] << 4) | nibbles[1]);
87+
}
88+
89+
return true;
90+
}
91+
92+
inline std::string serializePdu(const Pdu& pdu)
93+
{
94+
std::string out;
95+
96+
for (auto&& it : pdu)
97+
{
98+
out += it.first;
99+
out += ' ';
100+
out += hexEncode(it.second);
101+
out += '\n';
102+
}
103+
104+
return out;
105+
}
106+
107+
inline bool parsePdu(const std::string& str, Pdu& pdu)
108+
{
109+
pdu.clear();
110+
111+
std::string::size_type start = 0;
112+
while (start < str.size())
113+
{
114+
auto end = str.find('\n', start);
115+
if (end == std::string::npos) end = str.size();
116+
117+
auto line = str.substr(start, end - start);
118+
start = end + 1;
119+
120+
if (line.empty()) continue;
121+
122+
auto sep = line.find(' ');
123+
if (sep == std::string::npos) return false;
124+
125+
std::string value;
126+
if (!hexDecode(line.substr(sep + 1), value)) return false;
127+
128+
pdu[line.substr(0, sep)] = value;
129+
}
130+
131+
return true;
132+
}
133+
134+
// Convenience accessor, returns an empty string for a missing field.
135+
inline std::string getPduField(const Pdu& pdu, const std::string& name)
136+
{
137+
auto it = pdu.find(name);
138+
return (it == pdu.end()) ? std::string() : it->second;
139+
}
140+
} // namespace ix

‎docker/Dockerfile.alpine‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ FROM alpine:3.12 as build
22

33
RUN apk add --no-cache \
44
gcc g++ musl-dev linux-headers \
5-
cmake mbedtls-dev make zlib-dev python3-dev ninja git
5+
cmake mbedtls-dev make zlib-dev python3-dev ninja
66

77
RUN addgroup -S app && \
88
adduser -S -G app app && \

0 commit comments

Comments
 (0)