| File: Rpc\NativeLanguageCatalog.cs | Web Access |
| Project: src\src\Aspire.Hosting.Native.Server\Aspire.Hosting.Native.Server.csproj (Aspire.Hosting.Native.Server) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; using System.IO.Hashing; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace Aspire.Hosting.Native.Rpc; /// <summary>Serves build-time language tooling metadata without loading providers into the native runtime.</summary> internal sealed class NativeLanguageCatalog { private readonly NativeLanguageRegistration[] _languages; private readonly Func<string, JsonObject> _readSdk; private JsonArray _providers = []; internal NativeLanguageCatalog(NativeLanguageRegistration[] languages, Func<string, JsonObject> readSdk) { _languages = languages.ToArray(); _readSdk = readSdk; var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (var language in _languages) { if (language.RuntimeSpec.ValueKind != JsonValueKind.Object || !language.RuntimeSpec.TryGetProperty("language", out var id) || !language.RuntimeSpec.TryGetProperty("codeGenLanguage", out var codeGenLanguage) || id.ValueKind != JsonValueKind.String || codeGenLanguage.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(id.GetString()) || !ids.Add(id.GetString()!) || string.IsNullOrWhiteSpace(codeGenLanguage.GetString()) || string.IsNullOrWhiteSpace(language.SdkResourcePrefix)) { throw new InvalidDataException("Language registrations must have unique IDs and a code generation target."); } } } public static NativeLanguageCatalog LoadEmbedded() => LoadEmbedded(null); public static NativeLanguageCatalog LoadEmbedded(string? generatedSdkDirectory) { var assembly = typeof(NativeLanguageCatalog).Assembly; using var stream = assembly.GetManifestResourceStream("NativeLanguages.json") ?? throw new InvalidOperationException("The executable is missing its language catalog."); var languages = JsonSerializer.Deserialize(stream, NativeLanguageJsonContext.Default.NativeLanguageRegistrationArray) ?? throw new InvalidDataException("Invalid native language catalog."); var catalog = new NativeLanguageCatalog(languages, prefix => { var files = ReadSdk(assembly, prefix); if (generatedSdkDirectory is not null) { foreach (var path in Directory.GetFiles(generatedSdkDirectory, "*.mts")) { files[Path.GetFileName(path)] = File.ReadAllText(path); } } return files; }); if (generatedSdkDirectory is not null) { catalog._providers = JsonNode.Parse(File.ReadAllText(Path.Combine(generatedSdkDirectory, "providers.json")))!.AsArray(); } return catalog; } public JsonArray ProviderConfiguration() => _providers.DeepClone().AsArray(); public JsonObject RuntimeSpec(string language) { var registration = _languages.SingleOrDefault(entry => string.Equals(entry.RuntimeSpec.GetProperty("language").GetString(), language, StringComparison.OrdinalIgnoreCase)) ?? throw new NotSupportedException("The requested guest runtime is not included in this server build."); // RuntimeSpec is opaque provider-owned metadata. Forward its exact shape // rather than interpreting guest runtime commands in the kernel. return JsonNode.Parse(registration.RuntimeSpec.GetRawText())!.AsObject(); } public JsonObject GeneratedSdk(string language) { var registrations = _languages.Where(entry => string.Equals(entry.RuntimeSpec.GetProperty("language").GetString(), language, StringComparison.OrdinalIgnoreCase) || string.Equals(entry.RuntimeSpec.GetProperty("codeGenLanguage").GetString(), language, StringComparison.OrdinalIgnoreCase)) .ToArray(); var prefixes = registrations.Select(entry => entry.SdkResourcePrefix) .Distinct(StringComparer.Ordinal).ToArray(); if (prefixes.Length != 1) { throw new NotSupportedException("The requested SDK is absent or ambiguous in this server build."); } return _readSdk(prefixes[0]).DeepClone().AsObject(); } private static JsonObject ReadSdk(Assembly assembly, string prefix) { var files = new JsonObject(); foreach (var name in assembly.GetManifestResourceNames().Where(name => name.StartsWith(prefix, StringComparison.Ordinal))) { using var reader = new StreamReader(assembly.GetManifestResourceStream(name)!); files[name[prefix.Length..]] = reader.ReadToEnd(); } if (files.Count == 0) { throw new InvalidOperationException("The native executable is missing its generated SDK."); } using var contract = assembly.GetManifestResourceStream("Aspire.Hosting.Native.Rpc.contract.json") ?? throw new InvalidOperationException("AppHost Server v2 is missing its ATS contract."); using var contents = new MemoryStream(); contract.CopyTo(contents); files["apphost-server.json"] = new JsonObject { ["generation"] = 2, ["contractId"] = Convert.ToHexString(XxHash3.Hash(contents.GetBuffer().AsSpan(0, checked((int)contents.Length)))), ["version"] = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? throw new InvalidOperationException("AppHost Server v2 is missing its product version.") }.ToJsonString(); return files; } } internal sealed record NativeLanguageRegistration(JsonElement RuntimeSpec, string SdkResourcePrefix); [JsonSerializable(typeof(NativeLanguageRegistration[]))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, RespectNullableAnnotations = true)] internal sealed partial class NativeLanguageJsonContext : JsonSerializerContext;