feat(version): single-source semver, ext-state stamp, show-version action
Version "0.9.01" sourced once from CMake via configure_file'd header; pure app_version module (parse/compare/classify) with CTest. Stamp rides the persist save seam under "reasampler"/"version"; absent stamp reads as pre-versioning. On-demand "show version" action, no startup print.
This commit is contained in:
+40
-2
@@ -1,10 +1,30 @@
|
|||||||
cmake_minimum_required(VERSION 3.19)
|
cmake_minimum_required(VERSION 3.19)
|
||||||
project(reaper_reasampler LANGUAGES CXX)
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Version — SINGLE SOURCE OF TRUTH (Phase V, V1). Edit REASAMPLER_VERSION here and
|
||||||
|
# nowhere else: it flows to the binary constant, the ext-state writing-version stamp,
|
||||||
|
# and the "show version" action via a configure_file'd header (below). The string is
|
||||||
|
# authoritative verbatim — leading zero preserved (Daniel-fixed: exactly "0.9.01",
|
||||||
|
# two-digit zero-padded patch). We deliberately do NOT reconstruct the display string
|
||||||
|
# from project(VERSION)'s numeric components, since CMake may normalize a numeric patch
|
||||||
|
# field; the string variable is what renders. project(VERSION ...) is still set (with a
|
||||||
|
# normalized 0.9.1 triple) for CMake hygiene / any downstream numeric use, but it is NOT
|
||||||
|
# the rendered source of truth.
|
||||||
|
set(REASAMPLER_VERSION "0.9.01")
|
||||||
|
|
||||||
|
project(reaper_reasampler VERSION 0.9.1 LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||||
|
|
||||||
|
# Generate version_generated.h from the one REASAMPLER_VERSION variable. Regenerated at
|
||||||
|
# configure time whenever the variable changes; the exact string is substituted verbatim.
|
||||||
|
configure_file(
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/version_generated.h.in
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/generated/version_generated.h
|
||||||
|
@ONLY)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Vendored dependencies — add as git submodules (see README):
|
# Vendored dependencies — add as git submodules (see README):
|
||||||
# git submodule add https://github.com/justinfrankel/reaper-sdk vendor/reaper-sdk
|
# git submodule add https://github.com/justinfrankel/reaper-sdk vendor/reaper-sdk
|
||||||
@@ -195,6 +215,19 @@ add_library(wav_trim STATIC src/wav_trim.cpp)
|
|||||||
target_include_directories(wav_trim PUBLIC src)
|
target_include_directories(wav_trim PUBLIC src)
|
||||||
target_link_libraries(wav_trim PUBLIC peaks)
|
target_link_libraries(wav_trim PUBLIC peaks)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2i) Pure app_version library — NO REAPER, NO SWELL. The Phase V (V1) version-identity
|
||||||
|
# core: re-exports the ONE CMake-sourced version string (via the configure_file'd
|
||||||
|
# version_generated.h) and owns the pure parse/compare/classify logic — the semver
|
||||||
|
# ordering a within-channel forward migration needs, and the three-way writing-version
|
||||||
|
# classification (PreVersioning / Unknown / Stamped) persist reads back from ext state.
|
||||||
|
# Split out so the exact-string fidelity + parse/compare are unit-tested outside the
|
||||||
|
# DAW; the ext-state write/read (persist) and the show-version action (main) are shell.
|
||||||
|
# Depends on the generated header in the build tree (PUBLIC so every consumer sees it).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(app_version STATIC src/app_version.cpp)
|
||||||
|
target_include_directories(app_version PUBLIC src ${CMAKE_CURRENT_BINARY_DIR}/generated)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 3) Standalone tests for the pure modules (run without launching REAPER).
|
# 3) Standalone tests for the pure modules (run without launching REAPER).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -267,6 +300,10 @@ add_executable(owned_manifest_tests tests/test_owned_manifest.cpp)
|
|||||||
target_link_libraries(owned_manifest_tests PRIVATE owned_manifest)
|
target_link_libraries(owned_manifest_tests PRIVATE owned_manifest)
|
||||||
add_test(NAME owned_manifest_tests COMMAND owned_manifest_tests)
|
add_test(NAME owned_manifest_tests COMMAND owned_manifest_tests)
|
||||||
|
|
||||||
|
add_executable(app_version_tests tests/test_app_version.cpp)
|
||||||
|
target_link_libraries(app_version_tests PRIVATE app_version)
|
||||||
|
add_test(NAME app_version_tests COMMAND app_version_tests)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
|
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -303,8 +340,9 @@ add_library(reaper_reasampler MODULE
|
|||||||
src/actions.cpp
|
src/actions.cpp
|
||||||
src/bank_book.cpp
|
src/bank_book.cpp
|
||||||
src/owned_manifest.cpp
|
src/owned_manifest.cpp
|
||||||
|
src/app_version.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings tail_control realtime_record bank_book wav_trim owned_manifest)
|
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings tail_control realtime_record bank_book wav_trim owned_manifest app_version)
|
||||||
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
||||||
set_target_properties(reaper_reasampler PROPERTIES PREFIX "" OUTPUT_NAME "reaper_reasampler")
|
set_target_properties(reaper_reasampler PROPERTIES PREFIX "" OUTPUT_NAME "reaper_reasampler")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// app_version.cpp — implementation of the pure version-identity core (Phase V, V1).
|
||||||
|
// See app_version.h for the contract. The version STRING itself comes from
|
||||||
|
// version_generated.h (produced by CMake configure_file from the one REASAMPLER_VERSION
|
||||||
|
// variable) — this TU just re-exports it and owns the pure parse/compare/classify logic.
|
||||||
|
|
||||||
|
#include "app_version.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "version_generated.h" // REASAMPLER_VERSION_STRING — configure_file'd from CMake
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
const std::string& appVersion() {
|
||||||
|
// Function-local static: initialized once from the compile-time string, returned by
|
||||||
|
// const ref so callers share the one authoritative instance. The macro is the exact
|
||||||
|
// CMake value, leading zero and all.
|
||||||
|
static const std::string kVersion = REASAMPLER_VERSION_STRING;
|
||||||
|
return kVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Version> parseVersion(const std::string& s) {
|
||||||
|
// Split on '.' into exactly three non-empty all-digit components. No sign, no
|
||||||
|
// whitespace, no trailing garbage. Leading zeros are allowed (0.9.01 parses to
|
||||||
|
// {0,9,1}) — the display fidelity of the zero is the string's job, not this parse's.
|
||||||
|
Version v;
|
||||||
|
int component = 0; // 0=major, 1=minor, 2=patch
|
||||||
|
long long acc = 0; // current component accumulator (long long guards overflow)
|
||||||
|
bool digitsInComponent = false;
|
||||||
|
int* const out[3] = {&v.major, &v.minor, &v.patch};
|
||||||
|
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '.') {
|
||||||
|
if (!digitsInComponent) return std::nullopt; // empty component (".", "1..2")
|
||||||
|
if (component >= 2) return std::nullopt; // too many dots
|
||||||
|
*out[component] = static_cast<int>(acc);
|
||||||
|
++component;
|
||||||
|
acc = 0;
|
||||||
|
digitsInComponent = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c < '0' || c > '9') return std::nullopt; // non-digit (sign, letter, ws)
|
||||||
|
acc = acc * 10 + (c - '0');
|
||||||
|
if (acc > 1'000'000'000LL) return std::nullopt; // absurdly large -> reject
|
||||||
|
digitsInComponent = true;
|
||||||
|
}
|
||||||
|
if (component != 2 || !digitsInComponent) return std::nullopt; // too few components
|
||||||
|
*out[2] = static_cast<int>(acc);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool versionLess(const Version& a, const Version& b) {
|
||||||
|
if (a.major != b.major) return a.major < b.major;
|
||||||
|
if (a.minor != b.minor) return a.minor < b.minor;
|
||||||
|
return a.patch < b.patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
WritingVersion classifyWritingVersion(const std::string& rawStamp) {
|
||||||
|
WritingVersion wv;
|
||||||
|
if (rawStamp.empty()) {
|
||||||
|
wv.kind = WritingVersion::Kind::PreVersioning; // no stamp -> pre-versioning
|
||||||
|
return wv;
|
||||||
|
}
|
||||||
|
std::optional<Version> parsed = parseVersion(rawStamp);
|
||||||
|
if (!parsed) {
|
||||||
|
wv.kind = WritingVersion::Kind::Unknown; // present but malformed -> ignored
|
||||||
|
wv.raw = rawStamp;
|
||||||
|
return wv;
|
||||||
|
}
|
||||||
|
wv.kind = WritingVersion::Kind::Stamped;
|
||||||
|
wv.raw = rawStamp;
|
||||||
|
wv.parsed = *parsed;
|
||||||
|
return wv;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
// app_version — the REAPER-free version-identity core (Phase V, V1). The single
|
||||||
|
// source of truth for the version STRING lives in CMake (a `REASAMPLER_VERSION`
|
||||||
|
// variable threaded in via configure_file -> version_generated.h); this module
|
||||||
|
// re-exports it as the canonical constant and owns every pure operation on it: the
|
||||||
|
// exact-string render, the parse/compare arithmetic a within-channel forward
|
||||||
|
// migration will lean on, and the "which version wrote this project" result that
|
||||||
|
// persist reads back from ext state (absent stamp = pre-versioning, never an error).
|
||||||
|
//
|
||||||
|
// PURE MODULE: NO REAPER types, NO SWELL, NO vendor/ includes. Standard library
|
||||||
|
// only. Builds and unit-tests without REAPER (mirror of bank_model / tail_control).
|
||||||
|
//
|
||||||
|
// Leading-zero fidelity (V1, Daniel-fixed): the displayed/stamped string is EXACTLY
|
||||||
|
// "0.9.01" — two-digit zero-padded patch. That exactness is why the version STRING is
|
||||||
|
// the authoritative artifact (sourced verbatim from the one CMake variable), not a
|
||||||
|
// reconstruction from numeric components — CMake's `project(VERSION)` may normalize a
|
||||||
|
// numeric patch field, so we never round-trip the string through integers to render it.
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// The canonical version string — EXACTLY the value of the CMake `REASAMPLER_VERSION`
|
||||||
|
// variable (see version_generated.h, produced by configure_file). One edit point:
|
||||||
|
// changing that variable changes this constant, the ext-state stamp, and the
|
||||||
|
// show-version action output with no other edits. Leading zero preserved verbatim.
|
||||||
|
const std::string& appVersion();
|
||||||
|
|
||||||
|
// A parsed semver triple. Kept minimal — major.minor.patch as integers, for ORDERING
|
||||||
|
// only. It deliberately does NOT round-trip back to the display string (the leading
|
||||||
|
// zero is a rendering concern owned by the authoritative string, not reconstructable
|
||||||
|
// from the integer patch). parseVersion returns nullopt on malformed input.
|
||||||
|
struct Version {
|
||||||
|
int major = 0;
|
||||||
|
int minor = 0;
|
||||||
|
int patch = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse "x.y.z" (each component a non-negative integer, leading zeros allowed) into a
|
||||||
|
// Version. Returns nullopt on anything malformed: wrong component count, non-digits, a
|
||||||
|
// leading `-`, empty components, or trailing garbage. Used for ordering two stamps and
|
||||||
|
// for validating a stored stamp before comparing.
|
||||||
|
std::optional<Version> parseVersion(const std::string& s);
|
||||||
|
|
||||||
|
// Numeric ordering by (major, minor, patch). a < b iff a precedes b. So
|
||||||
|
// 0.9.01 < 0.9.02 < 0.10.01 (numeric compare, NOT lexicographic — 10 > 9).
|
||||||
|
bool versionLess(const Version& a, const Version& b);
|
||||||
|
|
||||||
|
// The writing-version a project was last saved with, recovered from its ext-state
|
||||||
|
// stamp. A project saved before this feature shipped has NO stamp — that is the
|
||||||
|
// explicit `preVersioning` case (kind), not an error and not a warning. A present-but-
|
||||||
|
// malformed value is `unknown` (also silent — a corrupt stamp is ignored, never
|
||||||
|
// throws). A well-formed value is `stamped` and carries the exact stored string plus
|
||||||
|
// its parsed triple for comparison.
|
||||||
|
struct WritingVersion {
|
||||||
|
enum class Kind {
|
||||||
|
PreVersioning, // no stamp stored — a project written before versioning shipped
|
||||||
|
Unknown, // a stamp was stored but is not parseable — ignored, not an error
|
||||||
|
Stamped, // a well-formed stamp
|
||||||
|
};
|
||||||
|
Kind kind = Kind::PreVersioning;
|
||||||
|
std::string raw; // the exact stored string (empty for PreVersioning)
|
||||||
|
Version parsed; // meaningful only when kind == Stamped
|
||||||
|
};
|
||||||
|
|
||||||
|
// Classify a raw stored stamp value (exactly what GetProjExtState returned for the
|
||||||
|
// version key). Empty -> PreVersioning; non-empty but unparseable -> Unknown; parseable
|
||||||
|
// -> Stamped. Pure so the three-way classification is test-pinned; persist calls this
|
||||||
|
// with the raw ext-state read and never has to reason about the cases itself.
|
||||||
|
WritingVersion classifyWritingVersion(const std::string& rawStamp);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "actions.h"
|
#include "actions.h"
|
||||||
|
#include "app_version.h"
|
||||||
#include "bank_model.h"
|
#include "bank_model.h"
|
||||||
#include "bank_panel.h"
|
#include "bank_panel.h"
|
||||||
#include "capture.h"
|
#include "capture.h"
|
||||||
@@ -109,6 +110,13 @@ static int g_cmdCaptureTrackRealtime = 0;
|
|||||||
// the transport-stop. No-op (with a note) when nothing is in flight.
|
// the transport-stop. No-op (with a note) when nothing is in flight.
|
||||||
static int g_cmdCancelRealtime = 0;
|
static int g_cmdCancelRealtime = 0;
|
||||||
|
|
||||||
|
// Command id for the Phase V "show version" action. FOREVER-STABLE string. On demand
|
||||||
|
// ONLY — prints the CMake-sourced version string to the console when fired. This is the
|
||||||
|
// SOLE new console output the versioning wave adds; there is no unconditional startup
|
||||||
|
// version print (routine console chatter was deliberately removed — it pops the console
|
||||||
|
// window). The user copies this line into a bug report.
|
||||||
|
static int g_cmdShowVersion = 0;
|
||||||
|
|
||||||
// The persistence session (M4): owns the in-memory BankIndex and bridges it to
|
// The persistence session (M4): owns the in-memory BankIndex and bridges it to
|
||||||
// project ext state. A timer tick drives g_session.poll() to detect project
|
// project ext state. A timer tick drives g_session.poll() to detect project
|
||||||
// load / Save-As; capture adds Samples to g_session.bank() — which (B2) resolves to
|
// load / Save-As; capture adds Samples to g_session.bank() — which (B2) resolves to
|
||||||
@@ -771,6 +779,12 @@ static bool OnHookCommand(int command, int /*flag*/)
|
|||||||
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; }
|
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; }
|
||||||
if (command == g_cmdCaptureTrackRealtime) { RunCaptureRealtimeTrack(); return true; }
|
if (command == g_cmdCaptureTrackRealtime) { RunCaptureRealtimeTrack(); return true; }
|
||||||
if (command == g_cmdCancelRealtime) { RunCancelRealtime(); return true; }
|
if (command == g_cmdCancelRealtime) { RunCancelRealtime(); return true; }
|
||||||
|
if (command == g_cmdShowVersion)
|
||||||
|
{
|
||||||
|
// On-demand version readout — the ONLY version output on any path.
|
||||||
|
ShowConsoleMsg(("ReaSampler " + reasampler::appVersion() + "\n").c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// Design View action family (D4). Claims only its own ids; returns false for the
|
// Design View action family (D4). Claims only its own ids; returns false for the
|
||||||
// rest so this hook keeps looking (per the contract).
|
// rest so this hook keeps looking (per the contract).
|
||||||
if (reasampler::designViewHandleCommand(command)) return true;
|
if (reasampler::designViewHandleCommand(command)) return true;
|
||||||
@@ -795,6 +809,7 @@ static gaccel_register_t g_accelInsertSelected{};
|
|||||||
static gaccel_register_t g_accelInsertSelectedConform{};
|
static gaccel_register_t g_accelInsertSelectedConform{};
|
||||||
static gaccel_register_t g_accelCaptureTrackRealtime{};
|
static gaccel_register_t g_accelCaptureTrackRealtime{};
|
||||||
static gaccel_register_t g_accelCancelRealtime{};
|
static gaccel_register_t g_accelCancelRealtime{};
|
||||||
|
static gaccel_register_t g_accelShowVersion{};
|
||||||
|
|
||||||
extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||||
REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t* rec)
|
REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t* rec)
|
||||||
@@ -826,6 +841,9 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
|||||||
reasampler::designViewUnregisterActions(g_rec);
|
reasampler::designViewUnregisterActions(g_rec);
|
||||||
// Tear down the multi-bank action family (B3) — same mirror-unregister.
|
// Tear down the multi-bank action family (B3) — same mirror-unregister.
|
||||||
reasampler::bankUnregisterActions(g_rec);
|
reasampler::bankUnregisterActions(g_rec);
|
||||||
|
g_rec->Register("-gaccel", (void*)&g_accelShowVersion);
|
||||||
|
g_rec->Register("-command_id",
|
||||||
|
(void*)(REASAMPLER_ACTION_PREFIX "SHOW_VERSION"));
|
||||||
g_rec->Register("-gaccel", (void*)&g_accelCancelRealtime);
|
g_rec->Register("-gaccel", (void*)&g_accelCancelRealtime);
|
||||||
g_rec->Register("-command_id",
|
g_rec->Register("-command_id",
|
||||||
(void*)(REASAMPLER_ACTION_PREFIX "CANCEL_REALTIME_CAPTURE"));
|
(void*)(REASAMPLER_ACTION_PREFIX "CANCEL_REALTIME_CAPTURE"));
|
||||||
@@ -969,6 +987,19 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
|||||||
rec->Register("gaccel", (void*)&g_accelCancelRealtime);
|
rec->Register("gaccel", (void*)&g_accelCancelRealtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register the Phase V "show version" action (command_id -> gaccel -> hookcommand).
|
||||||
|
// On-demand only — prints the CMake-sourced version to the console when fired; no
|
||||||
|
// startup print. FOREVER-STABLE id string.
|
||||||
|
g_cmdShowVersion = rec->Register(
|
||||||
|
"command_id",
|
||||||
|
(void*)(REASAMPLER_ACTION_PREFIX "SHOW_VERSION"));
|
||||||
|
if (g_cmdShowVersion)
|
||||||
|
{
|
||||||
|
g_accelShowVersion.accel.cmd = g_cmdShowVersion;
|
||||||
|
g_accelShowVersion.desc = "ReaSampler: show version";
|
||||||
|
rec->Register("gaccel", (void*)&g_accelShowVersion);
|
||||||
|
}
|
||||||
|
|
||||||
// Register the Design View action family (D4): toggle/activate mode, tag/untag/
|
// Register the Design View action family (D4): toggle/activate mode, tag/untag/
|
||||||
// show-both selected tracks. Each mints its own command_id + gaccel; the single
|
// show-both selected tracks. Each mints its own command_id + gaccel; the single
|
||||||
// hookcommand below routes them via designViewHandleCommand. Registered before
|
// hookcommand below routes them via designViewHandleCommand. Registered before
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "app_version.h"
|
||||||
#include "capture_paths.h"
|
#include "capture_paths.h"
|
||||||
|
|
||||||
#define REAPERAPI_MINIMAL
|
#define REAPERAPI_MINIMAL
|
||||||
@@ -226,6 +227,13 @@ bool ReaSamplerSession::saveToActiveProject() {
|
|||||||
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
|
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
|
||||||
kProjExtOwnedKey, ownedJson.c_str());
|
kProjExtOwnedKey, ownedJson.c_str());
|
||||||
|
|
||||||
|
// Phase V (V1): stamp the WRITING version — the build producing this save — under the
|
||||||
|
// version key, on the SAME seam as the keys above so the stamp and MarkProjectDirty
|
||||||
|
// stay paired (no drifting ad-hoc SetProjExtState). appVersion() is the one CMake-
|
||||||
|
// sourced constant; every save records the exact current build into the .rpp.
|
||||||
|
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
|
||||||
|
kProjExtVersionKey, appVersion().c_str());
|
||||||
|
|
||||||
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -315,6 +323,16 @@ void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDi
|
|||||||
// (R-B) must re-read the restored manifest so it matches the rolled-back bank state.
|
// (R-B) must re-read the restored manifest so it matches the rolled-back bank state.
|
||||||
owned_ = loadOwnedManifest(static_cast<ReaProject*>(proj));
|
owned_ = loadOwnedManifest(static_cast<ReaProject*>(proj));
|
||||||
|
|
||||||
|
// Phase V (V1): recover the writing-version stamp on EVERY load path (peer-symmetry
|
||||||
|
// with tail_/view_ above). An absent stamp classifies as PreVersioning, a malformed
|
||||||
|
// one as Unknown — both silent, no console warning (a pre-versioning project is not
|
||||||
|
// an error). getProjExtStateString returns "" for an absent key, which is exactly the
|
||||||
|
// PreVersioning input classifyWritingVersion expects. proj == nullptr -> "" -> default.
|
||||||
|
writingVersion_ = classifyWritingVersion(
|
||||||
|
proj ? getProjExtStateString(static_cast<ReaProject*>(proj), kProjExtNamespace,
|
||||||
|
kProjExtVersionKey)
|
||||||
|
: std::string{});
|
||||||
|
|
||||||
if (!proj) {
|
if (!proj) {
|
||||||
book_ = BankBook{};
|
book_ = BankBook{};
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "app_version.h"
|
||||||
#include "bank_book.h"
|
#include "bank_book.h"
|
||||||
#include "bank_model.h"
|
#include "bank_model.h"
|
||||||
#include "owned_manifest.h"
|
#include "owned_manifest.h"
|
||||||
@@ -70,6 +71,14 @@ inline constexpr const char* kProjExtTailKey = "tail_setting";
|
|||||||
// graceful, but the attribution safety net is lost until the next capture rebuilds it).
|
// graceful, but the attribution safety net is lost until the next capture rebuilds it).
|
||||||
inline constexpr const char* kProjExtOwnedKey = "owned_files";
|
inline constexpr const char* kProjExtOwnedKey = "owned_files";
|
||||||
|
|
||||||
|
// The ext-state key holding the ReaSampler version that last WROTE this project
|
||||||
|
// (Phase V, V1). Written on every save alongside the banks/view/tail keys, so every
|
||||||
|
// saved .rpp records which build produced its state — the seam a future within-channel
|
||||||
|
// forward migration keys off ("this was written by 0.9.01, I am 0.9.05"). An absent
|
||||||
|
// key is the explicit pre-versioning case (a project saved before this shipped), read
|
||||||
|
// silently, never an error. FOREVER-STABLE key string once shipped.
|
||||||
|
inline constexpr const char* kProjExtVersionKey = "version";
|
||||||
|
|
||||||
// The ext-state key holding a GUID we mint per project to establish CONTENT-BASED
|
// The ext-state key holding a GUID we mint per project to establish CONTENT-BASED
|
||||||
// project identity (REAPER exposes no stable per-project GUID). poll() uses it to
|
// project identity (REAPER exposes no stable per-project GUID). poll() uses it to
|
||||||
// tell a genuine Save-As (same GUID, new .rpp path) apart from a project switch
|
// tell a genuine Save-As (same GUID, new .rpp path) apart from a project switch
|
||||||
@@ -143,6 +152,15 @@ public:
|
|||||||
OwnedFileManifest& owned() { return owned_; }
|
OwnedFileManifest& owned() { return owned_; }
|
||||||
const OwnedFileManifest& owned() const { return owned_; }
|
const OwnedFileManifest& owned() const { return owned_; }
|
||||||
|
|
||||||
|
// The ReaSampler version that last WROTE the active project, recovered from its
|
||||||
|
// ext-state stamp on load (Phase V, V1). PreVersioning when the project carries no
|
||||||
|
// stamp (saved before this feature), Unknown for a malformed stamp, Stamped with the
|
||||||
|
// exact stored string otherwise — all silent, never an error. Replaced on every load
|
||||||
|
// path (peer-symmetry with bank_/view_/tail_); default PreVersioning for an unsaved
|
||||||
|
// or never-loaded session. Exposed so a future migration step (or diagnostics) can
|
||||||
|
// reason about the origin build without re-reading ext state.
|
||||||
|
const WritingVersion& writingVersion() const { return writingVersion_; }
|
||||||
|
|
||||||
// Serialize the current book (under the `banks` key), view model, and tail setting
|
// Serialize the current book (under the `banks` key), view model, and tail setting
|
||||||
// to the active project's ext state (namespace "reasampler"), and clear the retired
|
// to the active project's ext state (namespace "reasampler"), and clear the retired
|
||||||
// legacy `bank_index` key. Non-destructive beyond writing our own ext-state keys.
|
// legacy `bank_index` key. Non-destructive beyond writing our own ext-state keys.
|
||||||
@@ -208,6 +226,12 @@ private:
|
|||||||
// the in-memory set matches disk. Absent key -> empty is graceful (older project).
|
// the in-memory set matches disk. Absent key -> empty is graceful (older project).
|
||||||
OwnedFileManifest owned_;
|
OwnedFileManifest owned_;
|
||||||
|
|
||||||
|
// The writing-version stamp recovered on load (Phase V). Default PreVersioning;
|
||||||
|
// loadFromProject replaces it on every load path (peer to bank_/view_/tail_), so
|
||||||
|
// switching to a pre-versioning project reports PreVersioning rather than inheriting
|
||||||
|
// the previous project's stamp. Read-only to consumers via writingVersion().
|
||||||
|
WritingVersion writingVersion_;
|
||||||
|
|
||||||
// The project identity last observed by poll(), used to detect load/Save-As.
|
// The project identity last observed by poll(), used to detect load/Save-As.
|
||||||
// The GUID is the PRIMARY signal (a different stored GUID = a different project
|
// The GUID is the PRIMARY signal (a different stored GUID = a different project
|
||||||
// of record = Load, immune to pointer recycling). The pointer disambiguates the
|
// of record = Load, immune to pointer recycling). The pointer disambiguates the
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
// version_generated.h.in — configure_file TEMPLATE. CMake substitutes @REASAMPLER_VERSION@
|
||||||
|
// (the ONE source-of-truth variable in CMakeLists.txt) and writes the result to the build
|
||||||
|
// tree as version_generated.h. DO NOT edit the generated header — edit REASAMPLER_VERSION
|
||||||
|
// in CMakeLists.txt; that single edit re-generates this and re-threads the exact string
|
||||||
|
// (leading zero preserved verbatim) into the binary constant, the ext-state stamp, and the
|
||||||
|
// show-version action. See app_version.h / .cpp.
|
||||||
|
|
||||||
|
#define REASAMPLER_VERSION_STRING "@REASAMPLER_VERSION@"
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// Standalone tests for reasampler::app_version — no REAPER, no framework. Covers the
|
||||||
|
// Phase V (V1) pure version-identity core: the exact-string fidelity of the CMake-sourced
|
||||||
|
// constant (leading-zero preserved), the semver parse/compare arithmetic a within-channel
|
||||||
|
// forward migration leans on, and the three-way writing-version classification persist reads
|
||||||
|
// back from ext state (PreVersioning / Unknown / Stamped). The ext-state I/O (persist) and
|
||||||
|
// the show-version action (main) are DAW-verified shell.
|
||||||
|
|
||||||
|
#include "../src/app_version.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace reasampler;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// --- appVersion: exact-string fidelity, leading zero preserved ----------------
|
||||||
|
|
||||||
|
static void testVersionConstantRendersExactString() {
|
||||||
|
// The Daniel-fixed string: EXACTLY "0.9.01", two-digit zero-padded patch. This is
|
||||||
|
// the whole point of sourcing the STRING (not reconstructing from numeric components):
|
||||||
|
// a normalized "0.9.1" here would be a leading-zero-fidelity regression, and this
|
||||||
|
// assertion fails against the actual CMake-configured value, not a re-derivation.
|
||||||
|
CHECK(appVersion() == "0.9.01");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- parseVersion: well-formed, leading zeros, and rejection ------------------
|
||||||
|
|
||||||
|
static void testParseWellFormed() {
|
||||||
|
auto v = parseVersion("0.9.01");
|
||||||
|
CHECK(v.has_value());
|
||||||
|
CHECK(v && v->major == 0 && v->minor == 9 && v->patch == 1); // "01" -> 1
|
||||||
|
auto w = parseVersion("1.10.2");
|
||||||
|
CHECK(w.has_value());
|
||||||
|
CHECK(w && w->major == 1 && w->minor == 10 && w->patch == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testParseRejectsMalformed() {
|
||||||
|
CHECK(!parseVersion("").has_value()); // empty
|
||||||
|
CHECK(!parseVersion("1.2").has_value()); // too few components
|
||||||
|
CHECK(!parseVersion("1.2.3.4").has_value()); // too many components
|
||||||
|
CHECK(!parseVersion("1..2").has_value()); // empty middle component
|
||||||
|
CHECK(!parseVersion(".1.2").has_value()); // empty leading component
|
||||||
|
CHECK(!parseVersion("1.2.").has_value()); // empty trailing component
|
||||||
|
CHECK(!parseVersion("1.2.x").has_value()); // non-digit
|
||||||
|
CHECK(!parseVersion("-1.2.3").has_value()); // sign
|
||||||
|
CHECK(!parseVersion("1.2.3-beta").has_value()); // trailing garbage
|
||||||
|
CHECK(!parseVersion("v1.2.3").has_value()); // prefix
|
||||||
|
CHECK(!parseVersion(" 1.2.3").has_value()); // leading whitespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- versionLess: NUMERIC ordering (10 > 9, not lexicographic) ----------------
|
||||||
|
|
||||||
|
static void testOrderingByPatch() {
|
||||||
|
// 0.9.02 > 0.9.01 (the per-test-build patch increment the spec calls out).
|
||||||
|
auto a = parseVersion("0.9.01");
|
||||||
|
auto b = parseVersion("0.9.02");
|
||||||
|
CHECK(a && b);
|
||||||
|
CHECK(a && b && versionLess(*a, *b));
|
||||||
|
CHECK(a && b && !versionLess(*b, *a));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testOrderingIsNumericNotLexicographic() {
|
||||||
|
// 0.10.01 > 0.9.02 — the case a string compare would get WRONG ("10" < "9"
|
||||||
|
// lexicographically). This assertion fails if compare regressed to string order.
|
||||||
|
auto a = parseVersion("0.9.02");
|
||||||
|
auto b = parseVersion("0.10.01");
|
||||||
|
CHECK(a && b);
|
||||||
|
CHECK(a && b && versionLess(*a, *b));
|
||||||
|
// And a major bump outranks a large minor: 1.0.0 > 0.99.99.
|
||||||
|
auto c = parseVersion("0.99.99");
|
||||||
|
auto d = parseVersion("1.0.0");
|
||||||
|
CHECK(c && d && versionLess(*c, *d));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testOrderingIrreflexive() {
|
||||||
|
// Equal versions are not less-than either way.
|
||||||
|
auto a = parseVersion("0.9.01");
|
||||||
|
auto b = parseVersion("0.9.01");
|
||||||
|
CHECK(a && b && !versionLess(*a, *b));
|
||||||
|
CHECK(a && b && !versionLess(*b, *a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- classifyWritingVersion: the three-way stamp classification ----------------
|
||||||
|
|
||||||
|
static void testAbsentStampIsPreVersioning() {
|
||||||
|
// An absent key -> getProjExtStateString returns "" -> PreVersioning (a project
|
||||||
|
// saved before this feature). Silent, not an error, no throw.
|
||||||
|
WritingVersion wv = classifyWritingVersion("");
|
||||||
|
CHECK(wv.kind == WritingVersion::Kind::PreVersioning);
|
||||||
|
CHECK(wv.raw.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testMalformedStampIsUnknown() {
|
||||||
|
// A present-but-unparseable stamp -> Unknown (ignored, never a throw). Keeps the raw
|
||||||
|
// value for diagnostics but does not pretend it is a version.
|
||||||
|
WritingVersion wv = classifyWritingVersion("garbage-not-a-version");
|
||||||
|
CHECK(wv.kind == WritingVersion::Kind::Unknown);
|
||||||
|
CHECK(wv.raw == "garbage-not-a-version");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testWellFormedStampIsStamped() {
|
||||||
|
// A well-formed stamp -> Stamped, carrying the EXACT stored string plus the parsed
|
||||||
|
// triple for comparison. Round-trips the current version through classify.
|
||||||
|
WritingVersion wv = classifyWritingVersion("0.9.01");
|
||||||
|
CHECK(wv.kind == WritingVersion::Kind::Stamped);
|
||||||
|
CHECK(wv.raw == "0.9.01");
|
||||||
|
CHECK(wv.parsed.major == 0 && wv.parsed.minor == 9 && wv.parsed.patch == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testStampedStampIsOrderableAgainstCurrent() {
|
||||||
|
// The migration use case: a stamp read back is comparable to the running build. An
|
||||||
|
// older stamp (0.9.01) precedes a newer one (0.9.05) numerically.
|
||||||
|
WritingVersion older = classifyWritingVersion("0.9.01");
|
||||||
|
WritingVersion newer = classifyWritingVersion("0.9.05");
|
||||||
|
CHECK(older.kind == WritingVersion::Kind::Stamped);
|
||||||
|
CHECK(newer.kind == WritingVersion::Kind::Stamped);
|
||||||
|
CHECK(versionLess(older.parsed, newer.parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testVersionConstantRendersExactString();
|
||||||
|
testParseWellFormed();
|
||||||
|
testParseRejectsMalformed();
|
||||||
|
testOrderingByPatch();
|
||||||
|
testOrderingIsNumericNotLexicographic();
|
||||||
|
testOrderingIrreflexive();
|
||||||
|
testAbsentStampIsPreVersioning();
|
||||||
|
testMalformedStampIsUnknown();
|
||||||
|
testWellFormedStampIsStamped();
|
||||||
|
testStampedStampIsOrderableAgainstCurrent();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("app_version: all tests passed\n");
|
||||||
|
else std::printf("app_version: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user