diff --git a/.github/workflows/grpc.yml b/.github/workflows/grpc.yml
new file mode 100644
index 000000000..e9b40e50d
--- /dev/null
+++ b/.github/workflows/grpc.yml
@@ -0,0 +1,41 @@
+name: grpc
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ workflow_dispatch:
+
+env:
+ config: Release
+ disable_test_parallelization: true
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET 8
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Setup .NET 9
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 9.0.x
+
+ - name: Setup .NET 10
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Run gRPC Tests
+ run: ./build.sh CIGrpc --framework net9.0
diff --git a/build/CITargets.cs b/build/CITargets.cs
index 47719414b..d4aae4a07 100644
--- a/build/CITargets.cs
+++ b/build/CITargets.cs
@@ -450,6 +450,17 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat
RunSingleProjectOneClassAtATime(leaderElectionTests, frameworkOverride: "net9.0");
});
+ Target CIGrpc => _ => _
+ .ProceedAfterFailure()
+ .Executes(() =>
+ {
+ var tests = RootDirectory / "src" / "Wolverine.Grpc.Tests" / "Wolverine.Grpc.Tests.csproj";
+
+ BuildTestProjects(tests);
+
+ RunSingleProjectOneClassAtATime(tests);
+ });
+
Target CIAzureServiceBus => _ => _
.ProceedAfterFailure()
.Executes(() =>
diff --git a/build/build.cs b/build/build.cs
index 914520cbb..4c13b6104 100644
--- a/build/build.cs
+++ b/build/build.cs
@@ -350,6 +350,7 @@ partial class Build : NukeBuild
Solution.Transports.Redis.Wolverine_Redis,
Solution.Transports.SignalR.Wolverine_SignalR,
Solution.Transports.NATS.Wolverine_Nats,
+ Solution.Grpc.Wolverine_Grpc,
Solution.Persistence.EFCore.Wolverine_EntityFrameworkCore,
Solution.Persistence.Polecat.Wolverine_Polecat
};
diff --git a/docs/guide/codegen.md b/docs/guide/codegen.md
index 7d71452c4..56c766916 100644
--- a/docs/guide/codegen.md
+++ b/docs/guide/codegen.md
@@ -10,6 +10,10 @@ your message handlers. Wolverine's [middleware strategy](/guide/handlers/middlew
middleware directly into the runtime pipeline without requiring the copious usage of adapter interfaces
that is prevalent in most other .NET frameworks.
+::: info
+This page covers Wolverine-specific use of code generation. The shared JasperFx code-generation library that backs it — [frames](https://shared-libs.jasperfx.net/codegen/frames.html), [variables](https://shared-libs.jasperfx.net/codegen/variables.html), [`MethodCall`](https://shared-libs.jasperfx.net/codegen/method-call.html), [generated types](https://shared-libs.jasperfx.net/codegen/generated-types.html), and the [`codegen` CLI command](https://shared-libs.jasperfx.net/codegen/cli.html) — is documented at [shared-libs.jasperfx.net/codegen](https://shared-libs.jasperfx.net/codegen/). Reach for it when you're authoring a custom `IVariableSource` or middleware frame.
+:::
+
That's great when everything is working as it should, but there's a couple issues:
1. The usage of the Roslyn compiler at runtime *can sometimes be slow* on its first usage. This can lead to sluggish *cold start*
@@ -301,7 +305,7 @@ JasperFx).
::: tip
All of these commands are from the JasperFx.CodeGeneration.Commands library that Wolverine adds as
-a dependency. This is shared with [Marten](https://martendb.io) as well.
+a dependency. This is shared with [Marten](https://martendb.io) as well. See the [`codegen` CLI reference](https://shared-libs.jasperfx.net/codegen/cli.html) for every subcommand and flag.
:::
To preview the generated source code, use this command line usage from the root directory of your .NET project:
@@ -401,6 +405,8 @@ For Console applications or non-standard project structures, you may need to cus
### Using CritterStackDefaults
+`CritterStackDefaults` is the shared entry point for opinionated defaults across the Critter Stack (Wolverine, Marten, Polecat, …). Full reference: [shared-libs.jasperfx.net/configuration/critter-stack-defaults](https://shared-libs.jasperfx.net/configuration/critter-stack-defaults.html).
+
You can configure the output path globally for all Critter Stack tools:
diff --git a/docs/guide/command-line.md b/docs/guide/command-line.md
index 5f3d9628a..a9c49e7b2 100644
--- a/docs/guide/command-line.md
+++ b/docs/guide/command-line.md
@@ -6,6 +6,10 @@ With help from its [JasperFx](https://github.com/JasperFx) team mate [Oakton](ht
tools. To get started, apply Oakton as the command line parser in your applications as shown in the last line of code in this
sample application bootstrapping from Wolverine's [Getting Started](/tutorials/getting-started):
+::: info
+This page covers the Wolverine-specific CLI surface. The underlying JasperFx command-line library that backs `RunJasperFxCommands` — including how to [author your own commands](https://shared-libs.jasperfx.net/cli/writing-commands.html), [argument/flag handling](https://shared-libs.jasperfx.net/cli/arguments-flags.html), and [environment checks](https://shared-libs.jasperfx.net/cli/environment-checks.html) — is documented at [shared-libs.jasperfx.net/cli](https://shared-libs.jasperfx.net/cli/). When a command accepts a generic flag not listed on this page, that's where to look first.
+:::
+
```cs
diff --git a/docs/guide/durability/efcore/migrations.md b/docs/guide/durability/efcore/migrations.md
index 8fe80f84a..a4be4eaa1 100644
--- a/docs/guide/durability/efcore/migrations.md
+++ b/docs/guide/durability/efcore/migrations.md
@@ -105,4 +105,4 @@ dotnet run -- resources list
dotnet run -- resources clear
```
-These commands manage both Wolverine's internal tables and your EF Core entity tables together.
+These commands manage both Wolverine's internal tables and your EF Core entity tables together. For the finer-grained Weasel commands (`db-apply`, `db-assert`, `db-patch`, `db-dump`, `db-list`) — useful for CI deploy gates and exporting DDL — see the [Weasel CLI reference](https://weasel.jasperfx.net/cli/).
diff --git a/docs/guide/durability/managing.md b/docs/guide/durability/managing.md
index 76d275cbe..d3f12c812 100644
--- a/docs/guide/durability/managing.md
+++ b/docs/guide/durability/managing.md
@@ -115,13 +115,24 @@ The available commands are:
storage Administer the envelope storage
```
-There's admittedly some duplication here with different options coming from [Oakton](https://jasperfx.github.io/oakton) itself, the [Weasel.CommandLine](https://weasel.jasperfx.net/) library,
+There's admittedly some duplication here with different options coming from [Oakton](https://jasperfx.github.io/oakton) itself, the [Weasel.CommandLine](https://weasel.jasperfx.net/cli/) library,
and the `storage` command from Wolverine itself. To build out the schema objects for [message persistence](/guide/durability/), you
can use this command to apply any outstanding database changes necessary to bring the database schema to the Wolverine configuration:
```bash
dotnet run -- db-apply
```
+
+::: info
+The `db-apply`, `db-assert`, `db-patch`, `db-dump`, and `db-list` commands come from Weasel. See the per-command references at:
+
+- [`db-apply`](https://weasel.jasperfx.net/cli/db-apply.html) — apply all outstanding changes to the configured database(s)
+- [`db-assert`](https://weasel.jasperfx.net/cli/db-assert.html) — assert the live schema matches the configuration (good for CI deploy gates)
+- [`db-patch`](https://weasel.jasperfx.net/cli/db-patch.html) — emit a SQL patch + rollback file for pending changes
+- [`db-dump`](https://weasel.jasperfx.net/cli/db-dump.html) — dump the full DDL for the configured database(s)
+- [`db-list`](https://weasel.jasperfx.net/cli/db-list.html) — list configured databases
+:::
+
> NOTE: See the [Exporting SQL Scripts](#exporting-sql-scripts) section down the page for details of applying migrations when integrating with Marten
or this option -- but just know that this will also clear out any existing message data:
diff --git a/docs/guide/handlers/middleware.md b/docs/guide/handlers/middleware.md
index 1ac5f14c7..fe4bbcac0 100644
--- a/docs/guide/handlers/middleware.md
+++ b/docs/guide/handlers/middleware.md
@@ -359,7 +359,7 @@ on an individual handler method or apply to all handler methods on the same hand
## Custom Code Generation
-For more advanced usage, you can drop down to the JasperFx.CodeGeneration `Frame` model to directly inject code.
+For more advanced usage, you can drop down to the JasperFx.CodeGeneration `Frame` model to directly inject code. The `Frame` model itself — plus `Variable`, `MethodCall`, and the built-in frames Wolverine composes with — is documented in depth at [shared-libs.jasperfx.net/codegen](https://shared-libs.jasperfx.net/codegen/).
The first step is to create a JasperFx.CodeGeneration `Frame` class that generates that code around the inner message or HTTP handler:
diff --git a/src/Wolverine.Grpc.Tests/GrpcTransportCompliance.cs b/src/Wolverine.Grpc.Tests/GrpcTransportCompliance.cs
new file mode 100644
index 000000000..1d14f2532
--- /dev/null
+++ b/src/Wolverine.Grpc.Tests/GrpcTransportCompliance.cs
@@ -0,0 +1,33 @@
+using Wolverine.ComplianceTests.Compliance;
+using Wolverine.Grpc;
+using Xunit;
+
+public class GrpcComplianceFixture : TransportComplianceFixture, IAsyncLifetime
+{
+ public const int ReceiverPort = 5150;
+ public const int SenderPort = 5151;
+
+ public GrpcComplianceFixture() : base(new Uri($"grpc://localhost:{ReceiverPort}"), 30)
+ {
+ }
+
+ public async Task InitializeAsync()
+ {
+ OutboundAddress = new Uri($"grpc://localhost:{ReceiverPort}");
+
+ await ReceiverIs(opts =>
+ {
+ opts.ListenAtGrpcPort(ReceiverPort);
+ });
+
+ await SenderIs(opts =>
+ {
+ opts.ListenAtGrpcPort(SenderPort).UseForReplies();
+ opts.PublishAllMessages().ToGrpcEndpoint("localhost", ReceiverPort);
+ });
+ }
+
+ public new Task DisposeAsync() => Task.CompletedTask;
+}
+
+public class GrpcTransportCompliance : TransportCompliance;
diff --git a/src/Wolverine.Grpc.Tests/Wolverine.Grpc.Tests.csproj b/src/Wolverine.Grpc.Tests/Wolverine.Grpc.Tests.csproj
index 5ecfb2f37..baf788a15 100644
--- a/src/Wolverine.Grpc.Tests/Wolverine.Grpc.Tests.csproj
+++ b/src/Wolverine.Grpc.Tests/Wolverine.Grpc.Tests.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/Wolverine.Grpc/GrpcEndpoint.cs b/src/Wolverine.Grpc/GrpcEndpoint.cs
new file mode 100644
index 000000000..0aa8faa91
--- /dev/null
+++ b/src/Wolverine.Grpc/GrpcEndpoint.cs
@@ -0,0 +1,37 @@
+using Microsoft.Extensions.Logging;
+using Wolverine.Configuration;
+using Wolverine.Grpc.Internals;
+using Wolverine.Runtime;
+using Wolverine.Transports;
+using Wolverine.Transports.Sending;
+
+namespace Wolverine.Grpc;
+
+public class GrpcEndpoint : Endpoint
+{
+ public GrpcEndpoint(Uri uri) : base(uri, EndpointRole.Application)
+ {
+ Host = uri.Host;
+ Port = uri.IsDefaultPort ? 5000 : uri.Port;
+ }
+
+ public string Host { get; }
+ public int Port { get; }
+
+ public override async ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver)
+ {
+ var listener = new GrpcListener(Uri, Port, receiver, runtime.LoggerFactory.CreateLogger());
+ await listener.StartAsync();
+ return listener;
+ }
+
+ protected override ISender CreateSender(IWolverineRuntime runtime)
+ {
+ return new GrpcSender(Uri, Host, Port, runtime.LoggerFactory.CreateLogger());
+ }
+
+ protected override bool supportsMode(EndpointMode mode)
+ {
+ return mode is EndpointMode.Inline or EndpointMode.BufferedInMemory;
+ }
+}
diff --git a/src/Wolverine.Grpc/GrpcListenerConfiguration.cs b/src/Wolverine.Grpc/GrpcListenerConfiguration.cs
new file mode 100644
index 000000000..8bf209dc4
--- /dev/null
+++ b/src/Wolverine.Grpc/GrpcListenerConfiguration.cs
@@ -0,0 +1,10 @@
+using Wolverine.Configuration;
+
+namespace Wolverine.Grpc;
+
+public class GrpcListenerConfiguration : ListenerConfiguration
+{
+ public GrpcListenerConfiguration(GrpcEndpoint endpoint) : base(endpoint)
+ {
+ }
+}
diff --git a/src/Wolverine.Grpc/GrpcSubscriberConfiguration.cs b/src/Wolverine.Grpc/GrpcSubscriberConfiguration.cs
new file mode 100644
index 000000000..267bb5c72
--- /dev/null
+++ b/src/Wolverine.Grpc/GrpcSubscriberConfiguration.cs
@@ -0,0 +1,10 @@
+using Wolverine.Configuration;
+
+namespace Wolverine.Grpc;
+
+public class GrpcSubscriberConfiguration : SubscriberConfiguration
+{
+ public GrpcSubscriberConfiguration(GrpcEndpoint endpoint) : base(endpoint)
+ {
+ }
+}
diff --git a/src/Wolverine.Grpc/GrpcTransport.cs b/src/Wolverine.Grpc/GrpcTransport.cs
new file mode 100644
index 000000000..9ab6a278c
--- /dev/null
+++ b/src/Wolverine.Grpc/GrpcTransport.cs
@@ -0,0 +1,42 @@
+using JasperFx.Core;
+using Wolverine.Configuration;
+using Wolverine.Runtime;
+using Wolverine.Transports;
+
+namespace Wolverine.Grpc;
+
+public class GrpcTransport : TransportBase
+{
+ private readonly LightweightCache _endpoints;
+
+ public GrpcTransport() : base("grpc", "gRPC Transport", [])
+ {
+ _endpoints = new LightweightCache(uri => new GrpcEndpoint(uri));
+ }
+
+ protected override IEnumerable endpoints() => _endpoints;
+
+ protected override GrpcEndpoint findEndpointByUri(Uri uri) => _endpoints[uri];
+
+ public GrpcEndpoint EndpointForLocalPort(int port)
+ {
+ var uri = new Uri($"grpc://localhost:{port}");
+ return _endpoints[uri];
+ }
+
+ public GrpcEndpoint EndpointFor(string host, int port)
+ {
+ var uri = new Uri($"grpc://{host}:{port}");
+ return _endpoints[uri];
+ }
+
+ public override ValueTask InitializeAsync(IWolverineRuntime runtime)
+ {
+ foreach (var endpoint in _endpoints)
+ {
+ endpoint.Compile(runtime);
+ }
+
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/src/Wolverine.Grpc/GrpcTransportExtensions.cs b/src/Wolverine.Grpc/GrpcTransportExtensions.cs
new file mode 100644
index 000000000..52120821c
--- /dev/null
+++ b/src/Wolverine.Grpc/GrpcTransportExtensions.cs
@@ -0,0 +1,34 @@
+using JasperFx.Core.Reflection;
+using Wolverine.Configuration;
+
+namespace Wolverine.Grpc;
+
+public static class GrpcTransportExtensions
+{
+ ///
+ /// Configure Wolverine to listen for gRPC messages on the specified port.
+ /// The listener starts an embedded gRPC server bound to all interfaces.
+ ///
+ public static GrpcListenerConfiguration ListenAtGrpcPort(this WolverineOptions options, int port)
+ {
+ var transport = options.Transports.GetOrCreate();
+ var endpoint = transport.EndpointForLocalPort(port);
+ endpoint.IsListener = true;
+ return new GrpcListenerConfiguration(endpoint);
+ }
+
+ ///
+ /// Publish messages to the specified gRPC endpoint (host:port).
+ ///
+ public static GrpcSubscriberConfiguration ToGrpcEndpoint(
+ this IPublishToExpression publishing,
+ string host,
+ int port)
+ {
+ var transports = publishing.As().Parent.Transports;
+ var transport = transports.GetOrCreate();
+ var endpoint = transport.EndpointFor(host, port);
+ publishing.To(endpoint.Uri);
+ return new GrpcSubscriberConfiguration(endpoint);
+ }
+}
diff --git a/src/Wolverine.Grpc/Internals/GrpcListener.cs b/src/Wolverine.Grpc/Internals/GrpcListener.cs
new file mode 100644
index 000000000..dad0e3001
--- /dev/null
+++ b/src/Wolverine.Grpc/Internals/GrpcListener.cs
@@ -0,0 +1,74 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Wolverine.Runtime;
+using Wolverine.Transports;
+
+namespace Wolverine.Grpc.Internals;
+
+internal class GrpcListener : IListener
+{
+ private readonly IReceiver _receiver;
+ private readonly ILogger _logger;
+ private WebApplication? _app;
+
+ public GrpcListener(Uri address, int port, IReceiver receiver, ILogger logger)
+ {
+ Address = address;
+ Port = port;
+ _receiver = receiver;
+ _logger = logger;
+ }
+
+ public int Port { get; }
+
+ public IHandlerPipeline? Pipeline => null;
+
+ public Uri Address { get; }
+
+ internal async Task StartAsync()
+ {
+ var builder = WebApplication.CreateSlimBuilder();
+ builder.Logging.ClearProviders();
+
+ builder.WebHost.ConfigureKestrel(opts =>
+ {
+ opts.ListenAnyIP(Port, o => o.Protocols = HttpProtocols.Http2);
+ });
+
+ builder.Services.AddGrpc();
+ builder.Services.AddSingleton(_receiver);
+ builder.Services.AddSingleton(this);
+
+ _app = builder.Build();
+ _app.MapGrpcService();
+
+ await _app.StartAsync();
+ _logger.LogInformation("gRPC transport listener started on port {Port}", Port);
+ }
+
+ public ValueTask CompleteAsync(Envelope envelope) => ValueTask.CompletedTask;
+
+ public ValueTask DeferAsync(Envelope envelope) => ValueTask.CompletedTask;
+
+ public async ValueTask StopAsync()
+ {
+ if (_app != null)
+ {
+ _logger.LogInformation("Stopping gRPC transport listener on port {Port}", Port);
+ await _app.StopAsync();
+ }
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_app != null)
+ {
+ await _app.StopAsync();
+ await _app.DisposeAsync();
+ _app = null;
+ }
+ }
+}
diff --git a/src/Wolverine.Grpc/Internals/GrpcSender.cs b/src/Wolverine.Grpc/Internals/GrpcSender.cs
new file mode 100644
index 000000000..f8eae9f0d
--- /dev/null
+++ b/src/Wolverine.Grpc/Internals/GrpcSender.cs
@@ -0,0 +1,52 @@
+using Google.Protobuf;
+using Grpc.Net.Client;
+using Microsoft.Extensions.Logging;
+using Wolverine.Runtime.Serialization;
+using Wolverine.Transports.Sending;
+
+namespace Wolverine.Grpc.Internals;
+
+internal class GrpcSender : ISender, IDisposable
+{
+ private readonly ILogger _logger;
+ private readonly GrpcChannel _channel;
+ private readonly WolverineTransport.WolverineTransportClient _client;
+
+ public GrpcSender(Uri destination, string host, int port, ILogger logger)
+ {
+ Destination = destination;
+ _logger = logger;
+ _channel = GrpcChannel.ForAddress($"http://{host}:{port}");
+ _client = new WolverineTransport.WolverineTransportClient(_channel);
+ }
+
+ public bool SupportsNativeScheduledSend => false;
+
+ public Uri Destination { get; }
+
+ public async Task PingAsync()
+ {
+ try
+ {
+ var result = await _client.PingAsync(new PingRequest());
+ return result.Success;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Ping to {Destination} failed", Destination);
+ return false;
+ }
+ }
+
+ public async ValueTask SendAsync(Envelope envelope)
+ {
+ var data = EnvelopeSerializer.Serialize(envelope);
+ var message = new WolverineMessage { Data = ByteString.CopyFrom(data) };
+ await _client.SendAsync(message);
+ }
+
+ public void Dispose()
+ {
+ _channel.Dispose();
+ }
+}
diff --git a/src/Wolverine.Grpc/Internals/WolverineGrpcTransportService.cs b/src/Wolverine.Grpc/Internals/WolverineGrpcTransportService.cs
new file mode 100644
index 000000000..354d75aa8
--- /dev/null
+++ b/src/Wolverine.Grpc/Internals/WolverineGrpcTransportService.cs
@@ -0,0 +1,37 @@
+using Google.Protobuf;
+using Grpc.Core;
+using Wolverine.Runtime.Serialization;
+using Wolverine.Transports;
+
+namespace Wolverine.Grpc.Internals;
+
+internal class WolverineGrpcTransportService : WolverineTransport.WolverineTransportBase
+{
+ private readonly IReceiver _receiver;
+ private readonly IListener _listener;
+
+ public WolverineGrpcTransportService(IReceiver receiver, IListener listener)
+ {
+ _receiver = receiver;
+ _listener = listener;
+ }
+
+ public override async Task Send(WolverineMessage request, ServerCallContext context)
+ {
+ var envelope = EnvelopeSerializer.Deserialize(request.Data.ToByteArray());
+ await _receiver.ReceivedAsync(_listener, envelope);
+ return new Ack { Success = true };
+ }
+
+ public override async Task SendBatch(WolverineMessageBatch request, ServerCallContext context)
+ {
+ var envelopes = EnvelopeSerializer.ReadMany(request.Data.ToByteArray());
+ await _receiver.ReceivedAsync(_listener, envelopes);
+ return new Ack { Success = true };
+ }
+
+ public override Task Ping(PingRequest request, ServerCallContext context)
+ {
+ return Task.FromResult(new Ack { Success = true });
+ }
+}
diff --git a/src/Wolverine.Grpc/Internals/wolverine.proto b/src/Wolverine.Grpc/Internals/wolverine.proto
new file mode 100644
index 000000000..45314da11
--- /dev/null
+++ b/src/Wolverine.Grpc/Internals/wolverine.proto
@@ -0,0 +1,23 @@
+syntax = "proto3";
+option csharp_namespace = "Wolverine.Grpc.Internals";
+package wolverine_grpc;
+
+service WolverineTransport {
+ rpc Send(WolverineMessage) returns (Ack);
+ rpc SendBatch(WolverineMessageBatch) returns (Ack);
+ rpc Ping(PingRequest) returns (Ack);
+}
+
+message WolverineMessage {
+ bytes data = 1;
+}
+
+message WolverineMessageBatch {
+ bytes data = 1;
+}
+
+message PingRequest {}
+
+message Ack {
+ bool success = 1;
+}
diff --git a/src/Wolverine.Grpc/Wolverine.Grpc.csproj b/src/Wolverine.Grpc/Wolverine.Grpc.csproj
index 95a74dc91..2b479042b 100644
--- a/src/Wolverine.Grpc/Wolverine.Grpc.csproj
+++ b/src/Wolverine.Grpc/Wolverine.Grpc.csproj
@@ -1,19 +1,34 @@
- Code-first and proto-first gRPC service support for Wolverine
+ Code-first and proto-first gRPC service support for Wolverine, including gRPC message transport
WolverineFx.Grpc
+
+
+ <_Parameter1>Wolverine.Grpc.Tests
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+