diff --git a/README.md b/README.md
index 4ebae3aa..529d8ba5 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ ioxide hands you raw bytes and stays out of HTTP; when you want a framework on t
`ioxide.Kestrel` swaps the transport under an existing ASP.NET Core app with your endpoints
unchanged.
-> Linux 6.1+ · .NET 10 / .NET 11 · `0.14.236` - experimental
+> Linux 6.1+ · .NET 10 / .NET 11 · experimental
**[Documentation](https://mda2av.github.io/ioxide/)** - architecture, guides, and every example as
runnable code side by side.
diff --git a/src/ioxide/IoxideRuntime.cs b/src/ioxide/IoxideRuntime.cs
index b7dbb96d..27e16916 100644
--- a/src/ioxide/IoxideRuntime.cs
+++ b/src/ioxide/IoxideRuntime.cs
@@ -7,7 +7,20 @@ namespace ioxide;
///
public static class IoxideRuntime
{
- public const string Version = "0.14.236";
+ ///
+ /// The version of the NuGet package this assembly shipped in.
+ ///
+ ///
+ /// Generated from the Version property in ioxide.csproj by the GenerateVersionSource target
+ /// there, not written here. As a hand-kept literal it had to be remembered on every release and
+ /// was not: it still read "0.0.17" - untouched since the 0.0.x days - against packages on
+ /// 0.13.233, one of four statements of the same number with all four disagreeing (#224).
+ ///
+ /// Generated rather than read off the assembly at run time: AssemblyInformationalVersionAttribute
+ /// would be the obvious source, but reflecting over assembly attributes is what Native AOT
+ /// trims. A const costs nothing and survives trimming.
+ ///
+ public const string Version = IoxideVersion.Value;
// Wiring (a builder API will eventually wrap this):
// var reactor = new Reactor(id, config); // implements IRingHost
diff --git a/src/ioxide/ioxide.csproj b/src/ioxide/ioxide.csproj
index f4d525b8..c4d36256 100644
--- a/src/ioxide/ioxide.csproj
+++ b/src/ioxide/ioxide.csproj
@@ -23,4 +23,39 @@
+
+
+
+ <_IoxideVersionFile>$(IntermediateOutputPath)IoxideVersion.g.cs
+
+
+
+ <_IoxideVersionLines Include="// <auto-generated />" />
+ <_IoxideVersionLines Include="// Written by the GenerateVersionSource target in ioxide.csproj, from its Version property." />
+ <_IoxideVersionLines Include="// Do not edit, and do not commit: it lives under obj/." />
+ <_IoxideVersionLines Include="namespace ioxide%3B" />
+ <_IoxideVersionLines Include="internal static class IoxideVersion" />
+ <_IoxideVersionLines Include="{" />
+ <_IoxideVersionLines Include=" public const string Value = "$(Version)"%3B" />
+ <_IoxideVersionLines Include="}" />
+
+
+
+
+
+
+
+
+
diff --git a/tests/Ioxide.Tests.Unit/Program.cs b/tests/Ioxide.Tests.Unit/Program.cs
index b6205aae..f8636f77 100644
--- a/tests/Ioxide.Tests.Unit/Program.cs
+++ b/tests/Ioxide.Tests.Unit/Program.cs
@@ -11,6 +11,7 @@ private static int Main()
{
var runner = new Runner();
+ VersionTests.Register(runner);
DemuxParseTests.Register(runner);
MessageTests.Register(runner);
ResponseCapTests.Register(runner);
diff --git a/tests/Ioxide.Tests.Unit/VersionTests.cs b/tests/Ioxide.Tests.Unit/VersionTests.cs
new file mode 100644
index 00000000..dc91fead
--- /dev/null
+++ b/tests/Ioxide.Tests.Unit/VersionTests.cs
@@ -0,0 +1,75 @@
+using System.Text.RegularExpressions;
+using ioxide;
+
+namespace Ioxide.Tests;
+
+///
+/// reports the version the package was built as.
+///
+///
+/// It used to be a hand-kept literal, and literals rot: it read "0.0.17" against packages on
+/// 0.13.233, untouched since the 0.0.x days while three other statements of the same number moved
+/// without it (#224). It is now generated from ioxide.csproj's Version property, so the test worth
+/// having compares it against that property - if the generator stops running, or someone puts the
+/// literal back, these fail.
+///
+/// No reflection anywhere here, deliberately: the whole reason the version is generated rather than
+/// read off AssemblyInformationalVersionAttribute is that Native AOT trims attribute reflection, and
+/// a test that reached for it would be testing something the shipped configuration cannot do.
+///
+internal static class VersionTests
+{
+ public static void Register(Runner runner)
+ {
+ runner.Test("version: IoxideRuntime.Version matches ioxide.csproj", () =>
+ {
+ string csproj = FindCsproj();
+ Match declared = Regex.Match(File.ReadAllText(csproj), @"([^<]+)");
+
+ Assert.True(declared.Success, $"no element in {csproj}");
+ Assert.Equal(declared.Groups[1].Value, IoxideRuntime.Version);
+ });
+
+ runner.Test("version: it is a real version, not a placeholder", () =>
+ {
+ Assert.True(IoxideRuntime.Version.Length > 0, "Version is empty");
+
+ Assert.True(Version.TryParse(IoxideRuntime.Version, out Version? parsed) && parsed is not null,
+ $"Version is not parseable: '{IoxideRuntime.Version}'");
+
+ Assert.True(parsed!.Major > 0 || parsed.Minor > 0 || parsed.Build > 0,
+ $"Version is all zeroes: '{IoxideRuntime.Version}'");
+ });
+
+ runner.Test("version: no build metadata leaks into it", () =>
+ {
+ // The SDK appends "+" to the informational version from the repository's git
+ // metadata - this build's assembly attribute reads "0.14.236+75c9bf6...". Generating the
+ // const from the Version property sidesteps that entirely, and this pins it: callers
+ // compare the value against a package version, and no package is named with a sha on it.
+ Assert.True(!IoxideRuntime.Version.Contains('+'),
+ $"build metadata reached the reported version: '{IoxideRuntime.Version}'");
+ });
+ }
+
+ ///
+ /// Walks up from the test binary to the repo root, identified by the solution file beside it.
+ /// Plain file system, no reflection - see the note on the class.
+ ///
+ private static string FindCsproj()
+ {
+ for (DirectoryInfo? at = new(AppContext.BaseDirectory); at is not null; at = at.Parent)
+ {
+ if (File.Exists(Path.Combine(at.FullName, "ioxide.slnx")))
+ {
+ string csproj = Path.Combine(at.FullName, "src", "ioxide", "ioxide.csproj");
+
+ Assert.True(File.Exists(csproj), $"found the repo root at {at.FullName} but no {csproj}");
+ return csproj;
+ }
+ }
+
+ throw new Exception(
+ $"no ioxide.slnx above {AppContext.BaseDirectory} - this test reads the version out of the repo");
+ }
+}