|
| 1 | +using System.Globalization; |
| 2 | +using System.Reflection; |
| 3 | +using System.Runtime.InteropServices; |
| 4 | + |
| 5 | +using Spectara.Revela.Sdk.Hosting; |
| 6 | + |
| 7 | +namespace Spectara.Revela.Cli.Hosting; |
| 8 | + |
| 9 | +/// <summary> |
| 10 | +/// Default <see cref="IBuildInfo"/> implementation. |
| 11 | +/// </summary> |
| 12 | +/// <remarks> |
| 13 | +/// <para> |
| 14 | +/// Detects <see cref="HostKind"/> by reading the <c>Revela.HostKind</c> |
| 15 | +/// <see cref="AssemblyMetadataAttribute"/> from the entry assembly. Default |
| 16 | +/// when the attribute is absent is <see cref="HostKind.Standalone"/>. |
| 17 | +/// </para> |
| 18 | +/// <para> |
| 19 | +/// Both Cli and Cli.Embedded produce an executable named <c>revela</c>, so |
| 20 | +/// assembly-name-based detection is impossible. The metadata attribute is the |
| 21 | +/// single source of truth, set in <c>Cli.Embedded.csproj</c> via: |
| 22 | +/// </para> |
| 23 | +/// <code> |
| 24 | +/// <ItemGroup> |
| 25 | +/// <AssemblyMetadata Include="Revela.HostKind" Value="Embedded" /> |
| 26 | +/// </ItemGroup> |
| 27 | +/// </code> |
| 28 | +/// </remarks> |
| 29 | +internal sealed class BuildInfo : IBuildInfo |
| 30 | +{ |
| 31 | + private const string HostKindMetadataKey = "Revela.HostKind"; |
| 32 | + |
| 33 | + private const string BuildConfiguration = |
| 34 | +#if DEBUG |
| 35 | + "Debug"; |
| 36 | +#else |
| 37 | + "Release"; |
| 38 | +#endif |
| 39 | + |
| 40 | + public BuildInfo() |
| 41 | + : this(Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) |
| 42 | + { |
| 43 | + } |
| 44 | + |
| 45 | + /// <summary> |
| 46 | + /// Test seam: construct from a specific assembly, bypassing entry-assembly |
| 47 | + /// auto-detection. Used by BuildInfoTests. |
| 48 | + /// </summary> |
| 49 | + internal BuildInfo(Assembly entry) |
| 50 | + { |
| 51 | + Kind = DetectHostKind(entry); |
| 52 | + InformationalVersion = DetectInformationalVersion(entry); |
| 53 | + Version = StripBuildMetadata(InformationalVersion); |
| 54 | + Framework = RuntimeInformation.FrameworkDescription; |
| 55 | + Configuration = BuildConfiguration; |
| 56 | + RuntimeIdentifier = RuntimeInformation.RuntimeIdentifier; |
| 57 | + } |
| 58 | + |
| 59 | + /// <summary> |
| 60 | + /// Test seam: construct directly from raw values, fully bypassing assembly |
| 61 | + /// inspection. Used by BuildInfoTests for FormatVersionLine assertions |
| 62 | + /// across both <see cref="HostKind"/> values. |
| 63 | + /// </summary> |
| 64 | + internal BuildInfo( |
| 65 | + HostKind kind, |
| 66 | + string version, |
| 67 | + string informationalVersion, |
| 68 | + string framework, |
| 69 | + string configuration, |
| 70 | + string runtimeIdentifier) |
| 71 | + { |
| 72 | + Kind = kind; |
| 73 | + Version = version; |
| 74 | + InformationalVersion = informationalVersion; |
| 75 | + Framework = framework; |
| 76 | + Configuration = configuration; |
| 77 | + RuntimeIdentifier = runtimeIdentifier; |
| 78 | + } |
| 79 | + |
| 80 | + public HostKind Kind { get; } |
| 81 | + |
| 82 | + public string Version { get; } |
| 83 | + |
| 84 | + public string InformationalVersion { get; } |
| 85 | + |
| 86 | + public string Framework { get; } |
| 87 | + |
| 88 | + public string Configuration { get; } |
| 89 | + |
| 90 | + public string RuntimeIdentifier { get; } |
| 91 | + |
| 92 | + public string FormatVersionLine() |
| 93 | + { |
| 94 | + var suffix = Kind switch |
| 95 | + { |
| 96 | + HostKind.Embedded => " \u2014 embedded build", |
| 97 | + HostKind.Standalone => string.Empty, |
| 98 | + _ => string.Empty, |
| 99 | + }; |
| 100 | + return string.Create( |
| 101 | + CultureInfo.InvariantCulture, |
| 102 | + $"revela {Version} ({Framework}){suffix}"); |
| 103 | + } |
| 104 | + |
| 105 | + private static HostKind DetectHostKind(Assembly entry) |
| 106 | + { |
| 107 | + var value = entry |
| 108 | + .GetCustomAttributes<AssemblyMetadataAttribute>() |
| 109 | + .FirstOrDefault(a => string.Equals(a.Key, HostKindMetadataKey, StringComparison.Ordinal)) |
| 110 | + ?.Value; |
| 111 | + |
| 112 | + return string.Equals(value, nameof(HostKind.Embedded), StringComparison.Ordinal) |
| 113 | + ? HostKind.Embedded |
| 114 | + : HostKind.Standalone; |
| 115 | + } |
| 116 | + |
| 117 | + /// <summary>Test seam — exposes <see cref="DetectHostKind"/>.</summary> |
| 118 | + internal static HostKind DetectHostKindForTesting(Assembly entry) => DetectHostKind(entry); |
| 119 | + |
| 120 | + private static string DetectInformationalVersion(Assembly entry) |
| 121 | + { |
| 122 | + var info = entry.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion; |
| 123 | + if (!string.IsNullOrWhiteSpace(info)) |
| 124 | + { |
| 125 | + return info; |
| 126 | + } |
| 127 | + |
| 128 | + var version = entry.GetName().Version; |
| 129 | + return version is null |
| 130 | + ? "0.0.0" |
| 131 | + : version.ToString(3); |
| 132 | + } |
| 133 | + |
| 134 | + private static string StripBuildMetadata(string informational) |
| 135 | + { |
| 136 | + var plus = informational.IndexOf('+', StringComparison.Ordinal); |
| 137 | + return plus < 0 ? informational : informational[..plus]; |
| 138 | + } |
| 139 | + |
| 140 | + /// <summary>Test seam — exposes <see cref="StripBuildMetadata"/>.</summary> |
| 141 | + internal static string StripBuildMetadataForTesting(string informational) => StripBuildMetadata(informational); |
| 142 | +} |
0 commit comments