Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,8 @@ jobs:
if: ${{ success() || failure() }}
run: ./build.sh test-command-line
shell: bash

- name: smoke-test-aot
if: ${{ success() || failure() }}
run: ./build.sh smoke-test-aot
shell: bash
1 change: 1 addition & 0 deletions .nuke/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"NugetPack",
"NugetPush",
"Restore",
"SmokeTestAot",
"SmokeTestCommands",
"Test",
"TestCodegen",
Expand Down
23 changes: 22 additions & 1 deletion build/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ partial class Build : NukeBuild
.EnableNoRestore());
});

Target Test => _ => _.DependsOn(TestCore, TestCodegen, TestCommandLine, TestEvents);
Target Test => _ => _.DependsOn(TestCore, TestCodegen, TestCommandLine, TestEvents, SmokeTestAot);

Target TestCore => _ => _
.DependsOn(Compile)
Expand Down Expand Up @@ -107,6 +107,27 @@ partial class Build : NukeBuild
DotNet("run --framework net9.0 -- codegen preview --start", Solution.TestHarnesses.GeneratorTarget.Directory);
});

/// <summary>
/// AOT-clean consumer smoke test (jasperfx#213). The JasperFx.AotSmoke
/// project sets IsAotCompatible=true + promotes IL2026 / IL3050 / IL2046
/// / IL2070 / IL2075 (the full AOT analyzer set) to errors and exercises
/// a representative slice of the AOT-clean JasperFx + JasperFx.Events
/// surface. The build fails if a previously-AOT-clean API gains an
/// annotation, or if Program.cs is changed to call into a reflective
/// surface. Also runs the program to confirm runtime behavior is intact.
/// </summary>
Target SmokeTestAot => _ => _
.DependsOn(Compile)
.Executes(() =>
{
DotNetBuild(s => s
.SetProjectFile(Solution.TestHarnesses.JasperFx_AotSmoke)
.SetConfiguration(Configuration)
.EnableNoRestore());

DotNet("run --framework net10.0 --no-build", Solution.TestHarnesses.JasperFx_AotSmoke.Directory);
});

AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts";

Target NugetPack => _ => _
Expand Down
15 changes: 15 additions & 0 deletions jasperfx.sln
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JasperFx.SourceGeneration",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLineBenchmarks", "src\CommandLineBenchmarks\CommandLineBenchmarks.csproj", "{8D7BF9A9-0345-4FBA-9972-1F8413006DC2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JasperFx.AotSmoke", "src\JasperFx.AotSmoke\JasperFx.AotSmoke.csproj", "{8FB8216F-216F-480F-9519-A5893F7F3151}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -389,6 +391,18 @@ Global
{8D7BF9A9-0345-4FBA-9972-1F8413006DC2}.Release|x64.Build.0 = Release|Any CPU
{8D7BF9A9-0345-4FBA-9972-1F8413006DC2}.Release|x86.ActiveCfg = Release|Any CPU
{8D7BF9A9-0345-4FBA-9972-1F8413006DC2}.Release|x86.Build.0 = Release|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|x64.ActiveCfg = Debug|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|x64.Build.0 = Debug|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|x86.ActiveCfg = Debug|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|x86.Build.0 = Debug|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Release|Any CPU.Build.0 = Release|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Release|x64.ActiveCfg = Release|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Release|x64.Build.0 = Release|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Release|x86.ActiveCfg = Release|Any CPU
{8FB8216F-216F-480F-9519-A5893F7F3151}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -408,5 +422,6 @@ Global
{F0759B24-D9E2-4E42-B352-6C6B8138FD8C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{37E2EECE-FF24-4D39-96B6-BED9BCE8D1D4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{8D7BF9A9-0345-4FBA-9972-1F8413006DC2} = {A1BBEFB2-1FDB-4A32-8D00-DD5AA8E1E43A}
{8FB8216F-216F-480F-9519-A5893F7F3151} = {A1BBEFB2-1FDB-4A32-8D00-DD5AA8E1E43A}
EndGlobalSection
EndGlobal
34 changes: 34 additions & 0 deletions src/JasperFx.AotSmoke/JasperFx.AotSmoke.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>

<!--
AOT smoke test (jasperfx#213). Builds in CI and verifies that an app
consuming JasperFx + JasperFx.Events through the AOT-clean surfaces
(no reflective CommandFactory, no Activator.CreateInstance, no
MakeGenericType without a delegate-factory escape) compiles with no
IL2026 / IL2046 / IL2055 / IL2065 / IL2067 / IL2070 / IL2072 / IL2075
/ IL2090 / IL2091 / IL2111 / IL3050 / IL3051 warnings.

If a previously-AOT-clean API in JasperFx or JasperFx.Events later
gains a [RequiresUnreferencedCode] / [RequiresDynamicCode] annotation,
the build of this project will fail and the regression is caught in
our CI rather than downstream consumers'. New IL warnings introduced
by edits to this project (e.g. someone calls into a reflective surface
from Program.cs) are equally caught.
-->
<IsAotCompatible>true</IsAotCompatible>
<TrimMode>full</TrimMode>
<WarningsAsErrors>IL2026;IL2046;IL2055;IL2065;IL2067;IL2070;IL2072;IL2075;IL2090;IL2091;IL2111;IL3050;IL3051</WarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\JasperFx\JasperFx.csproj" />
<ProjectReference Include="..\JasperFx.Events\JasperFx.Events.csproj" />
</ItemGroup>

</Project>
74 changes: 74 additions & 0 deletions src/JasperFx.AotSmoke/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// AOT smoke test (jasperfx#213).
//
// This program touches a representative cross-section of the AOT-clean
// JasperFx + JasperFx.Events surface. The csproj sets IsAotCompatible=true
// and promotes the AOT analyzer warning codes to errors, so any change that
// adds [RequiresDynamicCode] / [RequiresUnreferencedCode] to an API exercised
// here — or any change to this file that calls into a reflective JasperFx
// surface — fails the build in CI.
//
// Intentionally *not* exercised here (those carry AOT annotations by design):
// - CommandFactory / CommandExecutor / CommandLineHostingExtensions
// (reflective command discovery; AOT-clean path is the source-generated
// DiscoveredCommands manifest, which is itself the smoke test for the
// JasperFx.SourceGenerator's CommandLine output)
// - GenericFactoryCache.BuildAs<T> (the delegate-factory overloads are
// annotated [RequiresDynamicCode] because the default factory calls
// MakeGenericType; an AOT consumer supplies its own AOT-safe factory)
// - SnapshotGate.Read / Write (System.Text.Json without a generation
// context — Marten and Wolverine consumers wrap these with their own
// STJ context or pre-serialized strings)

using JasperFx.CodeGeneration.Snapshots;
using JasperFx.Events;

// --- SnapshotGate.ComputeHash / Verify ----------------------------------
// Pure functions that compute SHA-256 over a canonical-input string and
// compare fingerprints. The AOT-clean substrate the codegen-snapshot
// contract (#243) is built on.

const string sampleInput = "marten-version=9.0.0␞store-name=AppDb";

string hashA = SnapshotGate.ComputeHash(sampleInput);
string hashB = SnapshotGate.ComputeHash(sampleInput);
if (hashA != hashB)
{
Console.Error.WriteLine($"SnapshotGate.ComputeHash is non-deterministic: '{hashA}' vs '{hashB}'.");
return 1;
}

var live = new SnapshotFingerprint(
ProductName: "marten",
ProductVersion: "9.0.0-alpha.1",
JasperFxVersion: "2.0.0-alpha.10",
ConfigHash: hashA,
SchemaVersion: SnapshotGate.CurrentSchemaVersion);

SnapshotVerdict firstBoot = SnapshotGate.Verify(live, persisted: null);
SnapshotVerdict accept = SnapshotGate.Verify(live, persisted: live);
SnapshotVerdict reject = SnapshotGate.Verify(live, persisted: live with { ConfigHash = "deadbeef" });

if (firstBoot != SnapshotVerdict.FirstBoot ||
accept != SnapshotVerdict.Accept ||
reject != SnapshotVerdict.RejectAndRegenerate)
{
Console.Error.WriteLine(
$"SnapshotGate.Verify regression: firstBoot={firstBoot}, accept={accept}, reject={reject}.");
return 1;
}

// --- JasperFx.Events.Event.For<T> ---------------------------------------
// Generic factory for IEvent<T>; AOT-clean for closed-over T.

IEvent<SampleEvent> evt = Event.For(new SampleEvent("hello"));
IEvent<SampleEvent> tenantEvt = Event.For("tenant-a", new SampleEvent("hello"));
if (evt.Data.Message != tenantEvt.Data.Message)
{
Console.Error.WriteLine("Event.For<T> regression.");
return 1;
}

Console.WriteLine($"JasperFx AOT smoke OK — ConfigHash={hashA[..16]}…");
return 0;

internal readonly record struct SampleEvent(string Message);
3 changes: 3 additions & 0 deletions src/JasperFx/CommandLine/ActivatorCommandCreator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using JasperFx.Core.Reflection;
using Spectre.Console;

Expand All @@ -11,6 +12,7 @@ public ActivatorCommandCreator()
Debug.WriteLine("What?");
}

[RequiresUnreferencedCode("Activator.CreateInstance(Type) requires the public parameterless constructor of commandType to survive trimming.")]
public IJasperFxCommand CreateCommand(Type commandType)
{
try
Expand All @@ -25,6 +27,7 @@ public IJasperFxCommand CreateCommand(Type commandType)
}
}

[RequiresUnreferencedCode("Activator.CreateInstance(Type) requires the public parameterless constructor of modelType to survive trimming.")]
public object CreateModel(Type modelType)
{
return Activator.CreateInstance(modelType)!;
Expand Down
17 changes: 16 additions & 1 deletion src/JasperFx/CommandLine/CommandExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using JasperFx.CommandLine.Parsing;
using System.Diagnostics.CodeAnalysis;
using JasperFx.CommandLine.Parsing;
using JasperFx.Core;
using Spectre.Console;

Expand Down Expand Up @@ -26,6 +27,8 @@ public CommandExecutor() : this(new CommandFactory())

public ICommandFactory Factory { get; }

[UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode",
Justification = "Spectre.Console.AnsiConsole.WriteException is only invoked on the error-display path; the outer try/catch falls back to Console.Write on Exception, so AOT consumers degrade gracefully.")]
private static async Task<int> execute(CommandRun run)
{
bool success;
Expand Down Expand Up @@ -70,6 +73,8 @@ private static async Task<int> execute(CommandRun run)
/// line arguments
/// </param>
/// <returns></returns>
[RequiresUnreferencedCode("Registers T via reflection and dispatches through CommandFactory.BuildRun, which depends on the command type's public constructor + input-type properties surviving trimming.")]
[RequiresDynamicCode("Enumerable command-argument parsing closes generic List<T> via MakeGenericType.")]
public static int ExecuteCommand<T>(string[] args, string? optsFile = null) where T : IJasperFxCommand
{
var factory = new CommandFactory();
Expand All @@ -94,6 +99,8 @@ public static int ExecuteCommand<T>(string[] args, string? optsFile = null) wher
/// line arguments
/// </param>
/// <returns></returns>
[RequiresUnreferencedCode("Registers T via reflection and dispatches through CommandFactory.BuildRun, which depends on the command type's public constructor + input-type properties surviving trimming.")]
[RequiresDynamicCode("Enumerable command-argument parsing closes generic List<T> via MakeGenericType.")]
public static Task<int> ExecuteCommandAsync<T>(string[] args, string? optsFile = null) where T : IJasperFxCommand
{
var factory = new CommandFactory();
Expand Down Expand Up @@ -172,6 +179,8 @@ internal static IEnumerable<string> ReadOptions(string? optionsFile)
/// </summary>
/// <param name="commandLine"></param>
/// <returns></returns>
[RequiresUnreferencedCode("Dispatches through ICommandFactory.BuildRun and reflective command instantiation.")]
[RequiresDynamicCode("Enumerable command-argument parsing closes generic List<T> via MakeGenericType.")]
public int Execute(string commandLine)
{
return ExecuteAsync(commandLine).GetAwaiter().GetResult();
Expand All @@ -182,6 +191,8 @@ public int Execute(string commandLine)
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
[RequiresUnreferencedCode("Dispatches through ICommandFactory.BuildRun and reflective command instantiation.")]
[RequiresDynamicCode("Enumerable command-argument parsing closes generic List<T> via MakeGenericType.")]
public int Execute(string[] args)
{
return ExecuteAsync(args).GetAwaiter().GetResult();
Expand All @@ -192,6 +203,8 @@ public int Execute(string[] args)
/// </summary>
/// <param name="commandLine"></param>
/// <returns></returns>
[RequiresUnreferencedCode("Dispatches through ICommandFactory.BuildRun and reflective command instantiation.")]
[RequiresDynamicCode("Enumerable command-argument parsing closes generic List<T> via MakeGenericType.")]
public Task<int> ExecuteAsync(string commandLine)
{
commandLine = applyOptions(commandLine);
Expand All @@ -205,6 +218,8 @@ public Task<int> ExecuteAsync(string commandLine)
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
[RequiresUnreferencedCode("Dispatches through ICommandFactory.BuildRun and reflective command instantiation.")]
[RequiresDynamicCode("Enumerable command-argument parsing closes generic List<T> via MakeGenericType.")]
public Task<int> ExecuteAsync(string[] args)
{
var run = Factory.BuildRun(ReadOptions(OptionsFile).Concat(args));
Expand Down
Loading
Loading