From f43163f2a500b80adaa8a3c73c3ed8c26c66b57d Mon Sep 17 00:00:00 2001 From: Zoltan Csizmadia Date: Sat, 5 Sep 2026 10:17:36 -0500 Subject: [PATCH] version: one source of truth, and --version on both binaries The version was written out by hand in several places and had drifted apart. CMake said 0.9.0, the handshake introduced itself as "wsldrive/0.1", and build-installer.ps1 defaulted to 0.1.0 - so README route 3 stamped a 0.1.0 installer onto a 0.9.0 build. Nothing printed a version at all, so a user could not say which build they had, and neither could a bug report. PROJECT_VERSION is now the only place the number appears. CMake generates core/version.hpp from it, and everything else reads that: - `wsldrive --version` / `wsldrived --version`, answered before any other argument handling so they still work on a machine where WinFsp, a distro or a reachable agent are not set up yet - which is when the question gets asked. - Both usage banners lead with the version. - The Hello handshake agent strings. - A Windows VERSIONINFO resource on all three executables, so Explorer's Details tab and (Get-Item wsldrive.exe).VersionInfo report the build. - build-installer.ps1 reads the version out of CMakeLists.txt instead of carrying its own default. Verified: `wsldrive --version` and `wsldrived --version` print 0.9.0 on both platforms; all three Windows binaries report FileVersion 0.9.0 with distinct descriptions; build-installer.ps1 resolves 0.9.0 with no -Version argument. Linux 124/124, Windows 122/122, conformance 31/0 Linux and 28/0 Windows. Closes #76 Co-Authored-By: Claude Opus 5 --- CMakeLists.txt | 5 +++++ scripts/build-installer.ps1 | 11 ++++++++++- src/agent/client.cpp | 6 +++++- src/agent/server.cpp | 3 ++- src/core/CMakeLists.txt | 2 +- src/core/version.hpp.in | 30 ++++++++++++++++++++++++++++++ src/tools/CMakeLists.txt | 22 ++++++++++++++++++++++ src/tools/version.rc.in | 34 ++++++++++++++++++++++++++++++++++ src/tools/wsldrive_main.cpp | 16 ++++++++++++++++ src/tools/wsldrived_main.cpp | 12 +++++++++++- tests/CMakeLists.txt | 1 + tests/version_test.cpp | 31 +++++++++++++++++++++++++++++++ 12 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 src/core/version.hpp.in create mode 100644 src/tools/version.rc.in create mode 100644 tests/version_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2172916..8b326c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,11 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Build type" FORCE) endif() +# One source of truth for the version: PROJECT_VERSION above. Everything that +# needs it reads the generated header rather than repeating the number. +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/core/version.hpp.in" + "${CMAKE_CURRENT_BINARY_DIR}/generated/core/version.hpp" @ONLY) + option(WSLDRIVE_BUILD_TESTS "Build unit tests" ON) option(WSLDRIVE_BUILD_BENCHMARKS "Build micro-benchmarks" ON) option(WSLDRIVE_WARNINGS_AS_ERRORS "Treat compiler and linker warnings as errors" ON) diff --git a/scripts/build-installer.ps1 b/scripts/build-installer.ps1 index 1080790..ab945c5 100644 --- a/scripts/build-installer.ps1 +++ b/scripts/build-installer.ps1 @@ -16,12 +16,21 @@ [CmdletBinding()] param( [string] $Config = 'release', - [string] $Version = '0.1.0', + # Defaults to the project version in CMakeLists.txt. Hard-coding it here is + # how the script came to stamp 0.1.0 onto a 0.9.0 build. + [string] $Version = '', [switch] $InstallInno ) $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot + +if (-not $Version) { + $cml = Get-Content (Join-Path $root 'CMakeLists.txt') -Raw + if ($cml -match '(?m)^\s*VERSION\s+(\d+\.\d+\.\d+)\s*$') { $Version = $Matches[1] } + else { throw "could not read VERSION from CMakeLists.txt; pass -Version explicitly" } +} +Write-Host "building installer for wsldrive $Version" $iss = Join-Path $root 'installer\wsldrive.iss' $binDir = Join-Path $root "build\msvc-$Config\src\tools" $linux = Join-Path $root 'build\linux-release\src\tools\wsldrive' # client (Direction B) diff --git a/src/agent/client.cpp b/src/agent/client.cpp index 010f3ba..86039dd 100644 --- a/src/agent/client.cpp +++ b/src/agent/client.cpp @@ -1,6 +1,7 @@ #include "agent/client.hpp" #include "core/path.hpp" +#include "core/version.hpp" #include #include @@ -62,7 +63,10 @@ Result RemoteRoot::connect(std::chrono::milliseconds timeout) { proto::Writer w(payload); proto::write_hello( w, proto::Hello{ - .protocol_version = proto::kVersion, .capabilities = 0, .agent = "wsldrive/0.1", .token = token_}); + .protocol_version = proto::kVersion, + .capabilities = 0, + .agent = agent_string("wsldrive"), + .token = token_}); auto p = request(proto::MsgType::Hello, payload, timeout); if (!p) return fail(p.error()); // An Error reply (e.g. a rejected token) is already decoded by request(). diff --git a/src/agent/server.cpp b/src/agent/server.cpp index f866798..62397d3 100644 --- a/src/agent/server.cpp +++ b/src/agent/server.cpp @@ -2,6 +2,7 @@ #include "agent/scanner.hpp" #include "core/path.hpp" +#include "core/version.hpp" #ifdef _WIN32 #include "platform/win/wide.hpp" #endif @@ -350,7 +351,7 @@ Result RootServer::handle(const net::Frame& f, net::FrameChannel& ch, bool return ch.send_with(proto::MsgType::HelloAck, f.header.request_id, [&](proto::Writer& w) { proto::write_hello(w, proto::Hello{.protocol_version = proto::kVersion, .capabilities = watching() ? 1u : 0u, - .agent = "wsldrived/0.1", .token = {}}); + .agent = agent_string("wsldrived"), .token = {}}); }); } case proto::MsgType::Ping: return ch.send(proto::MsgType::Pong, f.header.request_id, {}); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 58d1fdb..854e339 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -12,7 +12,7 @@ add_library(wsldrive_core STATIC add_library(wsldrive::core ALIAS wsldrive_core) target_include_directories(wsldrive_core - PUBLIC "${CMAKE_SOURCE_DIR}/src" + PUBLIC "${CMAKE_SOURCE_DIR}/src" "${CMAKE_BINARY_DIR}/generated" PRIVATE "${WSLDRIVE_BLAKE3_INCLUDE_DIR}") target_link_libraries(wsldrive_core PUBLIC wsldrive::options diff --git a/src/core/version.hpp.in b/src/core/version.hpp.in new file mode 100644 index 0000000..3f97e76 --- /dev/null +++ b/src/core/version.hpp.in @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +// Generated from src/core/version.hpp.in by CMake. The project version has one +// source of truth - PROJECT_VERSION in the top-level CMakeLists.txt - and this +// is how the rest of the build reads it: `--version` and the usage banners, the +// handshake agent strings, the Windows VERSIONINFO resource, and the default +// version stamped into the installer. + +namespace wsld { + +/// Project version, e.g. "0.9.0". +inline constexpr std::string_view kVersionString = "@PROJECT_VERSION@"; + +inline constexpr int kVersionMajor = @PROJECT_VERSION_MAJOR@; +inline constexpr int kVersionMinor = @PROJECT_VERSION_MINOR@; +inline constexpr int kVersionPatch = @PROJECT_VERSION_PATCH@; + +/// "/", for the free-form agent field of the Hello handshake. +/// Returns a reference to a function-local static, so the string_view a caller +/// stores in a message stays valid for the life of the process. +inline const std::string& agent_string(std::string_view name) { + static const std::string wsldrive_agent = "wsldrive/" + std::string(kVersionString); + static const std::string wsldrived_agent = "wsldrived/" + std::string(kVersionString); + return name == "wsldrived" ? wsldrived_agent : wsldrive_agent; +} + +} // namespace wsld diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 5d14f69..5a83bda 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -12,3 +12,25 @@ endif() if(WIN32) add_executable(wsldrivew WIN32 wsldrivew_main.cpp) endif() + +# Stamp each executable with a VERSIONINFO resource, from the same +# PROJECT_VERSION the binaries print. Without it Explorer's Details tab and +# (Get-Item wsldrive.exe).VersionInfo say nothing about which build this is. +if(WIN32) + enable_language(RC) + foreach(tgt wsldrive wsldrived wsldrivew) + if(TARGET ${tgt}) + set(WSLDRIVE_RC_NAME "${tgt}") + if(tgt STREQUAL "wsldrived") + set(WSLDRIVE_RC_DESCRIPTION "wsldrive agent (serves a directory tree)") + elseif(tgt STREQUAL "wsldrivew") + set(WSLDRIVE_RC_DESCRIPTION "wsldrive windowless launcher") + else() + set(WSLDRIVE_RC_DESCRIPTION "wsldrive command line tool") + endif() + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.rc.in" + "${CMAKE_CURRENT_BINARY_DIR}/${tgt}_version.rc" @ONLY) + target_sources(${tgt} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/${tgt}_version.rc") + endif() + endforeach() +endif() diff --git a/src/tools/version.rc.in b/src/tools/version.rc.in new file mode 100644 index 0000000..1d8a51c --- /dev/null +++ b/src/tools/version.rc.in @@ -0,0 +1,34 @@ +// Generated from version.rc.in by CMake, once per executable. Without it +// Explorer's Details tab and `(Get-Item wsldrive.exe).VersionInfo` report +// nothing, so a user cannot tell which build they are running - and neither can +// a support request. +#include + +VS_VERSION_INFO VERSIONINFO +FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0 +PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0 +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS 0 +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" // U.S. English, Unicode + BEGIN + VALUE "CompanyName", "wsldrive" + VALUE "FileDescription", "@WSLDRIVE_RC_DESCRIPTION@" + VALUE "FileVersion", "@PROJECT_VERSION@" + VALUE "InternalName", "@WSLDRIVE_RC_NAME@" + VALUE "OriginalFilename", "@WSLDRIVE_RC_NAME@.exe" + VALUE "ProductName", "wsldrive" + VALUE "ProductVersion", "@PROJECT_VERSION@" + VALUE "LegalCopyright", "MIT licensed" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END diff --git a/src/tools/wsldrive_main.cpp b/src/tools/wsldrive_main.cpp index d7e0ed6..49a9ea7 100644 --- a/src/tools/wsldrive_main.cpp +++ b/src/tools/wsldrive_main.cpp @@ -27,6 +27,8 @@ #include #include #include +#include "core/version.hpp" + #include #include #include @@ -34,7 +36,10 @@ namespace { +void print_version(const char* who) { std::printf("%s %s\n", who, std::string(wsld::kVersionString).c_str()); } + void usage() { + std::fprintf(stderr, "wsldrive %s\n", std::string(wsld::kVersionString).c_str()); std::fputs( "usage:\n" " wsldrive fetch (--connect | --listen ) [--watch] [--read ] [--lookups N]\n" @@ -53,6 +58,7 @@ void usage() { " (auto-launch the agent in the distro)\n" " add --hvsocket [--vm-guid ] (use the Hyper-V socket transport)\n" #endif + " wsldrive --version\n" , stderr); } @@ -85,6 +91,16 @@ void install_mount_signal_handler() { } // namespace int main(int argc, char** argv) { + // Answered before anything else, so it still works on a machine where the + // rest of the environment (WinFsp, a distro, a reachable agent) is not set + // up - which is exactly when someone needs to know which build they have. + if (argc >= 2) { + const std::string_view first = argv[1]; + if (first == "--version" || first == "-V" || first == "version") { + print_version("wsldrive"); + return 0; + } + } const std::string_view command = argc >= 2 ? std::string_view(argv[1]) : std::string_view{}; #ifdef _WIN32 diff --git a/src/tools/wsldrived_main.cpp b/src/tools/wsldrived_main.cpp index 49c9fad..f455506 100644 --- a/src/tools/wsldrived_main.cpp +++ b/src/tools/wsldrived_main.cpp @@ -5,6 +5,7 @@ #include "agent/server.hpp" #include "core/auth_token.hpp" +#include "core/version.hpp" #include "net/frame_channel.hpp" #include "net/socket.hpp" @@ -19,19 +20,28 @@ namespace { void usage() { + std::fprintf(stderr, "wsldrived %s\n", std::string(wsld::kVersionString).c_str()); std::fputs( "usage: wsldrived --root (--listen | --connect ) [--no-watch] [--exit-when-idle]\n" " [--cross-filesystems]\n" " endpoints: tcp://host:port | vsock://cid:port | hv://port\n" " --exit-when-idle: serve a single client session then exit (used by auto-launch)\n" " --cross-filesystems: also descend into mount points below --root. Off by default, so\n" - " serving / skips /proc, /sys, /dev, /run and foreign mounts like /mnt/c.\n", + " serving / skips /proc, /sys, /dev, /run and foreign mounts like /mnt/c.\n" + " --version: print the version and exit.\n", stderr); } } // namespace int main(int argc, char** argv) { + if (argc >= 2) { + const std::string_view first = argv[1]; + if (first == "--version" || first == "-V") { + std::printf("wsldrived %s\n", std::string(wsld::kVersionString).c_str()); + return 0; + } + } std::filesystem::path root; std::string listen, connect; bool watch = true; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c8434a5..7c27ac6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(core_tests protocol_test.cpp string_pool_test.cpp unicode_test.cpp + version_test.cpp win_names_test.cpp) target_link_libraries(core_tests PRIVATE wsldrive::core GTest::gtest_main) diff --git a/tests/version_test.cpp b/tests/version_test.cpp new file mode 100644 index 0000000..e8d0d2d --- /dev/null +++ b/tests/version_test.cpp @@ -0,0 +1,31 @@ +#include "core/version.hpp" + +#include + +#include +#include + +namespace wsld { +namespace { + +TEST(Version, IsWiredToTheProjectVersion) { + // The version used to be written out by hand in several places and drifted: + // the handshake said 0.1, the installer defaulted to 0.1.0, and the project + // was 0.9.0. Everything reads the generated header now, so this only has to + // check that the header carries a real version and that the agent strings are + // built from it rather than repeating a literal. + EXPECT_FALSE(kVersionString.empty()); + EXPECT_NE(kVersionString, "0.0.0"); + EXPECT_EQ(kVersionString.find_first_not_of("0123456789."), std::string_view::npos) + << "expected a dotted numeric version, got: " << kVersionString; + EXPECT_EQ(kVersionString, std::to_string(kVersionMajor) + "." + std::to_string(kVersionMinor) + "." + + std::to_string(kVersionPatch)); + + EXPECT_EQ(agent_string("wsldrive"), "wsldrive/" + std::string(kVersionString)); + EXPECT_EQ(agent_string("wsldrived"), "wsldrived/" + std::string(kVersionString)); + // The handshake stores a view into this, so it has to outlive the call. + EXPECT_EQ(&agent_string("wsldrive"), &agent_string("wsldrive")); +} + +} // namespace +} // namespace wsld