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
25 changes: 25 additions & 0 deletions docs/input/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,31 @@ Spectre.Console has an [analyzer](https://www.nuget.org/packages/Spectre.Console
common errors in writing console output from above such as using multiple live rendering widgets simultaneously,
or using the static `AnsiConsole` class when `IAnsiConsole` is available.

### Native AOT Support

Publishing your app as Native AOT with Spectre.Console produces an app that's self-contained and has been ahead-of-time (AOT) compiled to native code. Native AOT apps have faster startup time and smaller memory footprints. These apps can run on machines that don't have the .NET runtime installed.

To enable AOT support on your application, Add `<PublishAot>true</PublishAot>` to your project file.

```xml
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
```

Current Spectre.Console support for AOT:

* &#9745;&#65039; Spectre.Console
* &#10060; Spectre.Console.Cli
* &#9745;&#65039; Spectre.Console.Json
* &#9745;&#65039; Spectre.Console.ImageSharp

Spectre.Console.Cli relies on reflection and discovering types at runtime, preventing it from currently supporting AOT.

Spectre.Console supports AOT, but with the following limitations

* `WriteException` will output a simple stacktrace and ignore any `ExceptionFormats` set.

### Configuring the Windows Terminal For Unicode and Emoji Support

Windows Terminal supports Unicode and Emoji. However, the shells such as Powershell and cmd.exe do not.
Expand Down
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageVersion Include="PolySharp" Version="1.14.1" />
<PackageVersion Include="Shouldly" Version="4.2.1" />
<PackageVersion Include="Spectre.Verify.Extensions" Version="22.3.2-preview.0.1" />
<PackageVersion Include="Verify.Xunit" Version="28.2.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
<IsPackable>true</IsPackable>
<Description>A library that extends Spectre.Console with ImageSharp superpowers.</Description>
</PropertyGroup>

<PropertyGroup>
<IsAotCompatible Condition="'$(TargetFramework)' != 'netstandard2.0'" >true</IsAotCompatible>
<PolySharpIncludeRuntimeSupportedAttributes>true</PolySharpIncludeRuntimeSupportedAttributes>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<ImplicitUsings>true</ImplicitUsings>
<IsPackable>true</IsPackable>
<Description>A library that extends Spectre.Console with JSON superpowers.</Description>
</PropertyGroup>

<PropertyGroup>
<IsAotCompatible Condition="'$(TargetFramework)' != 'netstandard2.0'" >true</IsAotCompatible>
<PolySharpIncludeRuntimeSupportedAttributes>true</PolySharpIncludeRuntimeSupportedAttributes>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Spectre.Console\Internal\Extensions\CharExtensions.cs" Link="Internal\CharExtensions.cs" />
<Compile Include="..\..\Spectre.Console\Internal\Extensions\EnumerableExtensions.cs" Link="Internal\EnumerableExtensions.cs" />
Expand Down
3 changes: 3 additions & 0 deletions src/Spectre.Console.Cli/CommandApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ namespace Spectre.Console.Cli;
/// <summary>
/// The entry point for a command line application.
/// </summary>
#if !NETSTANDARD2_0
[RequiresDynamicCode("Spectre.Console.Cli relies on reflection. Use during trimming and AOT compilation is not supported and may result in unexpected behaviors.")]
#endif
public sealed class CommandApp : ICommandApp
{
private readonly Configurator _configurator;
Expand Down
3 changes: 3 additions & 0 deletions src/Spectre.Console.Cli/CommandAppOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ namespace Spectre.Console.Cli;
/// The entry point for a command line application with a default command.
/// </summary>
/// <typeparam name="TDefaultCommand">The type of the default command.</typeparam>
#if !NETSTANDARD2_0
[RequiresDynamicCode("Spectre.Console.Cli relies on reflection. Use during trimming and AOT compilation is not supported and may result in unexpected behaviors.")]
#endif
public sealed class CommandApp<TDefaultCommand> : ICommandApp
where TDefaultCommand : class, ICommand
{
Expand Down
5 changes: 4 additions & 1 deletion src/Spectre.Console.Cli/Spectre.Console.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<IsPackable>true</IsPackable>
</PropertyGroup>

<PropertyGroup>
<IsAotCompatible Condition="'$(TargetFramework)' != 'netstandard2.0'" >false</IsAotCompatible>
<IsTrimmable>false</IsTrimmable>
</PropertyGroup>
<ItemGroup Label="REMOVE THIS">
<InternalsVisibleTo Include="Spectre.Console.Cli.Tests" />
</ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/Spectre.Console/AnsiConsole.Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public static partial class AnsiConsole
/// </summary>
/// <param name="exception">The exception to write to the console.</param>
/// <param name="format">The exception format options.</param>
[RequiresDynamicCode(ExceptionFormatter.AotWarning)]
public static void WriteException(Exception exception, ExceptionFormats format = ExceptionFormats.Default)
{
Console.WriteException(exception, format);
Expand All @@ -20,6 +21,7 @@ public static void WriteException(Exception exception, ExceptionFormats format =
/// </summary>
/// <param name="exception">The exception to write to the console.</param>
/// <param name="settings">The exception settings.</param>
[RequiresDynamicCode(ExceptionFormatter.AotWarning)]
public static void WriteException(Exception exception, ExceptionSettings settings)
{
Console.WriteException(exception, settings);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static partial class AnsiConsoleExtensions
/// <param name="console">The console.</param>
/// <param name="exception">The exception to write to the console.</param>
/// <param name="format">The exception format options.</param>
[RequiresDynamicCode(ExceptionFormatter.AotWarning)]
public static void WriteException(this IAnsiConsole console, Exception exception, ExceptionFormats format = ExceptionFormats.Default)
{
console.Write(exception.GetRenderable(format));
Expand All @@ -22,6 +23,7 @@ public static void WriteException(this IAnsiConsole console, Exception exception
/// <param name="console">The console.</param>
/// <param name="exception">The exception to write to the console.</param>
/// <param name="settings">The exception settings.</param>
[RequiresDynamicCode(ExceptionFormatter.AotWarning)]
public static void WriteException(this IAnsiConsole console, Exception exception, ExceptionSettings settings)
{
console.Write(exception.GetRenderable(settings));
Expand Down
2 changes: 2 additions & 0 deletions src/Spectre.Console/Extensions/ExceptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static class ExceptionExtensions
/// <param name="exception">The exception to format.</param>
/// <param name="format">The exception format options.</param>
/// <returns>A <see cref="IRenderable"/> representing the exception.</returns>
[RequiresDynamicCode(ExceptionFormatter.AotWarning)]
public static IRenderable GetRenderable(this Exception exception, ExceptionFormats format = ExceptionFormats.Default)
{
if (exception is null)
Expand All @@ -30,6 +31,7 @@ public static IRenderable GetRenderable(this Exception exception, ExceptionForma
/// <param name="exception">The exception to format.</param>
/// <param name="settings">The exception settings.</param>
/// <returns>A <see cref="IRenderable"/> representing the exception.</returns>
[RequiresDynamicCode(ExceptionFormatter.AotWarning)]
public static IRenderable GetRenderable(this Exception exception, ExceptionSettings settings)
{
if (exception is null)
Expand Down
3 changes: 1 addition & 2 deletions src/Spectre.Console/Internal/DecorationTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ public static List<string> GetMarkupNames(Decoration decoration)
{
var result = new List<string>();

Enum.GetValues(typeof(Decoration))
.Cast<Decoration>()
EnumUtils.GetValues<Decoration>()
.Where(flag => (decoration & flag) != 0)
.ForEach(flag =>
{
Expand Down
2 changes: 1 addition & 1 deletion src/Spectre.Console/Internal/FileSize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ private FileSizePrefix DetectPrefix(double bytes)
bytes *= 8;
}

foreach (var prefix in (FileSizePrefix[])Enum.GetValues(typeof(FileSizePrefix)))
foreach (var prefix in EnumUtils.GetValues<FileSizePrefix>())
{
// Trying to find the largest unit, that the number of bytes can fit under. Ex. 40kb < 1mb
if (bytes < Math.Pow((int)_prefixBase, (int)prefix + 1))
Expand Down
15 changes: 15 additions & 0 deletions src/Spectre.Console/Internal/Polyfill/EnumHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Spectre.Console;

internal static class EnumUtils
{
public static T[] GetValues<T>()
where T : struct, Enum
{
return
#if NET6_0_OR_GREATER
Enum.GetValues<T>();
#else
(T[])Enum.GetValues(typeof(T));
#endif
}
}
98 changes: 97 additions & 1 deletion src/Spectre.Console/Internal/TypeConverterHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ namespace Spectre.Console;

internal static class TypeConverterHelper
{
internal const DynamicallyAccessedMemberTypes ConverterAnnotation = DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields;

internal static bool IsGetConverterSupported =>
!AppContext.TryGetSwitch("Spectre.Console.TypeConverterHelper.IsGetConverterSupported ", out var enabled) || enabled;

public static string ConvertToString<T>(T input)
{
var result = GetTypeConverter<T>().ConvertToInvariantString(input);
Expand Down Expand Up @@ -51,7 +56,7 @@ public static bool TryConvertFromStringWithCulture<T>(string input, CultureInfo?

public static TypeConverter GetTypeConverter<T>()
{
var converter = TypeDescriptor.GetConverter(typeof(T));
var converter = GetConverter();
if (converter != null)
{
return converter;
Expand All @@ -72,5 +77,96 @@ public static TypeConverter GetTypeConverter<T>()
}

throw new InvalidOperationException("Could not find type converter");

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087", Justification = "Feature switches are not currently supported in the analyzer")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "Feature switches are not currently supported in the analyzer")]
static TypeConverter? GetConverter()
{
if (IsGetConverterSupported)
{
// Spectre.Console.TypeConverterHelper.IsGetConverterSupported has been set so
// fallback to original behavior
return TypeDescriptor.GetConverter(typeof(T));
}

// otherwise try and use the intrinsic converter. if we can't find one, then
// try and use GetConverter.
var intrinsicConverter = GetIntrinsicConverter(typeof(T));
return intrinsicConverter ?? TypeDescriptor.GetConverter(typeof(T));
}
}

private delegate TypeConverter FuncWithDam([DynamicallyAccessedMembers(ConverterAnnotation)] Type type);

private static readonly Dictionary<Type, FuncWithDam> _intrinsicConverters;

static TypeConverterHelper()
{
_intrinsicConverters = new()
{
[typeof(bool)] = _ => new BooleanConverter(),
[typeof(byte)] = _ => new ByteConverter(),
[typeof(sbyte)] = _ => new SByteConverter(),
[typeof(char)] = _ => new CharConverter(),
[typeof(double)] = _ => new DoubleConverter(),
[typeof(string)] = _ => new StringConverter(),
[typeof(int)] = _ => new Int32Converter(),
[typeof(short)] = _ => new Int16Converter(),
[typeof(long)] = _ => new Int64Converter(),
[typeof(float)] = _ => new SingleConverter(),
[typeof(ushort)] = _ => new UInt16Converter(),
[typeof(uint)] = _ => new UInt32Converter(),
[typeof(ulong)] = _ => new UInt64Converter(),
[typeof(object)] = _ => new TypeConverter(),
[typeof(CultureInfo)] = _ => new CultureInfoConverter(),
[typeof(DateTime)] = _ => new DateTimeConverter(),
[typeof(DateTimeOffset)] = _ => new DateTimeOffsetConverter(),
[typeof(decimal)] = _ => new DecimalConverter(),
[typeof(TimeSpan)] = _ => new TimeSpanConverter(),
[typeof(Guid)] = _ => new GuidConverter(),
[typeof(Uri)] = _ => new UriTypeConverter(),
[typeof(Array)] = _ => new ArrayConverter(),
[typeof(ICollection)] = _ => new CollectionConverter(),
[typeof(Enum)] = CreateEnumConverter(),
#if !NETSTANDARD2_0
[typeof(Int128)] = _ => new Int128Converter(),
[typeof(Half)] = _ => new HalfConverter(),
[typeof(UInt128)] = _ => new UInt128Converter(),
[typeof(DateOnly)] = _ => new DateOnlyConverter(),
[typeof(TimeOnly)] = _ => new TimeOnlyConverter(),
[typeof(Version)] = _ => new VersionConverter(),
#endif
};
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111", Justification = "Delegate reflection is safe for all usages in this type.")]
private static FuncWithDam CreateEnumConverter() => ([DynamicallyAccessedMembers(ConverterAnnotation)] Type type) => new EnumConverter(type);

/// <summary>
/// A highly-constrained version of <see cref="TypeDescriptor.GetConverter(Type)" /> that only returns intrinsic converters.
/// </summary>
private static TypeConverter? GetIntrinsicConverter([DynamicallyAccessedMembers(ConverterAnnotation)] Type type)
{
if (type.IsArray)
{
type = typeof(Array);
}

if (typeof(ICollection).IsAssignableFrom(type))
{
type = typeof(ICollection);
}

if (type.IsEnum)
{
type = typeof(Enum);
}

if (_intrinsicConverters.TryGetValue(type, out var factory))
{
return factory(type);
}

return null;
}
}
9 changes: 8 additions & 1 deletion src/Spectre.Console/Spectre.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
<IsPackable>true</IsPackable>
<DefineConstants>$(DefineConstants)TRACE;WCWIDTH_VISIBILITY_INTERNAL</DefineConstants>
</PropertyGroup>

<PropertyGroup>
<IsAotCompatible Condition="'$(TargetFramework)' != 'netstandard2.0'" >true</IsAotCompatible>
<PolySharpIncludeRuntimeSupportedAttributes>true</PolySharpIncludeRuntimeSupportedAttributes>
</PropertyGroup>
<ItemGroup Label="REMOVE THIS">
<InternalsVisibleTo Include="$(AssemblyName).Tests"/>
</ItemGroup>
Expand All @@ -20,6 +23,10 @@
<PackageReference Include="Wcwidth.Sources">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="PolySharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
Expand Down
37 changes: 35 additions & 2 deletions src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
namespace Spectre.Console;

// ExceptionFormatter relies heavily on reflection of types unknown until runtime.
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2070:RequiresUnreferencedCode")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode")]
internal static class ExceptionFormatter
{
public const string AotWarning = "ExceptionFormatter is currently not supported for AOT.";

public static IRenderable Format(Exception exception, ExceptionSettings settings)
{
if (exception is null)
Expand Down Expand Up @@ -56,8 +63,16 @@ private static Grid GetStackFrames(Exception ex, ExceptionSettings settings)
}

var stackTrace = new StackTrace(ex, fNeedFileInfo: true);
var frames = stackTrace
.GetFrames()
var allFrames = stackTrace.GetFrames();
if (allFrames[0]?.GetMethod() == null)
{
// if we can't easily get the method for the frame, then we are in AOT
// fallback to using ToString method of each frame.
WriteAotFrames(grid, stackTrace.GetFrames(), styles);
return grid;
}

var frames = allFrames
.FilterStackFrames()
.ToList();

Expand Down Expand Up @@ -118,6 +133,23 @@ private static Grid GetStackFrames(Exception ex, ExceptionSettings settings)
return grid;
}

private static void WriteAotFrames(Grid grid, StackFrame?[] frames, ExceptionStyle styles)
{
foreach (var stackFrame in frames)
{
if (stackFrame == null)
{
continue;
}

var s = stackFrame.ToString();
s = s.Replace(" in file:line:column <filename unknown>:0:0", string.Empty).TrimEnd();
grid.AddRow(
$"[{styles.Dimmed.ToMarkup()}]at[/]",
s.EscapeMarkup());
}
}

private static void AppendParameters(StringBuilder builder, MethodBase? method, ExceptionSettings settings)
{
var typeColor = settings.Style.ParameterType.ToMarkup();
Expand Down Expand Up @@ -398,6 +430,7 @@ private static string GetMethodName(ref MethodBase method, out bool isAsync)
return builder.ToString();
}

[RequiresDynamicCode(ExceptionFormatter.AotWarning)]
private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType)
{
// https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L400-L455
Expand Down
Loading