Skip to content

Commit 629af72

Browse files
committed
AOT/Trimming compatibility
1 parent 5b9402e commit 629af72

30 files changed

Lines changed: 839 additions & 41 deletions
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
name: Test AOT package integration
2+
3+
run-name: TestAotPackageIntegration_${{ github.ref_name }}_${{ vars.LIBRARY_VERSION }}_${{ github.run_number }}
4+
5+
on:
6+
workflow_dispatch:
7+
push:
8+
branches:
9+
- master
10+
- develop
11+
paths:
12+
- 'DryWetMidi/**'
13+
- '.github/workflows/test-aot-package-integration.yml'
14+
- '.github/workflows/build-native-libs.yml'
15+
- '.github/workflows/build-package.yml'
16+
- '.github/actions/**'
17+
- 'Resources/PackageIntegrationTestUtilities/DwmAotApp/**'
18+
- 'Resources/Native/**'
19+
20+
concurrency:
21+
group: ${{ github.workflow }}-${{ github.ref }}
22+
cancel-in-progress: true
23+
24+
jobs:
25+
build-native-libs:
26+
name: Build Native Libraries
27+
uses: ./.github/workflows/build-native-libs.yml
28+
29+
build-package:
30+
name: Build NuGet Package
31+
needs: build-native-libs
32+
uses: ./.github/workflows/build-package.yml
33+
secrets: inherit
34+
35+
test-package-integration:
36+
name: Test package integration (${{ matrix.os }})
37+
needs: build-package
38+
runs-on: ${{ matrix.os }}
39+
timeout-minutes: 60
40+
strategy:
41+
fail-fast: false
42+
matrix:
43+
include:
44+
#- os: windows-11-arm
45+
# framework: Net
46+
# rid: win-arm64
47+
- os: macos-latest
48+
framework: Net
49+
rid: osx-arm64
50+
51+
steps:
52+
- name: Checkout
53+
uses: actions/checkout@v4
54+
55+
- name: Download NuGet package
56+
uses: actions/download-artifact@v4
57+
with:
58+
name: NuGetPackage
59+
path: ${{ github.workspace }}/NuGetPackage
60+
61+
- name: Setup framework
62+
id: framework
63+
uses: ./.github/actions/setup-framework
64+
with:
65+
framework: ${{ matrix.framework }}
66+
net-tfm: ${{ vars.BUILD_FRAMEWORK_NET_TFM || 'net8.0' }}
67+
net-sdk-version: ${{ vars.BUILD_FRAMEWORK_NET_SDK_VERSION || '8.0.x' }}
68+
netframework-tfm: ${{ vars.BUILD_FRAMEWORK_NETFRAMEWORK_TFM || 'net48' }}
69+
netframework-sdk-version: ${{ vars.BUILD_FRAMEWORK_NETFRAMEWORK_SDK_VERSION || '8.0.x' }}
70+
71+
- name: Add package to the project
72+
shell: pwsh
73+
run: |
74+
Write-Host "Adding local NuGet source with the package..."
75+
dotnet nuget add source "${{ github.workspace }}/NuGetPackage"
76+
77+
Write-Host "Adding Melanchall.DryWetMidi.${{ vars.LIBRARY_VERSION }} package reference to the project..."
78+
$path = Resolve-Path 'Resources\PackageIntegrationTestUtilities\DwmAotApp\DwmAotApp\DwmAotApp.csproj'
79+
dotnet add "$path" package Melanchall.DryWetMidi -v ${{ vars.LIBRARY_VERSION }}
80+
81+
- name: Check csproj package reference
82+
shell: pwsh
83+
run: |
84+
$path = Resolve-Path "Resources\PackageIntegrationTestUtilities\DwmAotApp\DwmAotApp\DwmAotApp.csproj"
85+
$expected = '<PackageReference Include="Melanchall.DryWetMidi" Version="${{ vars.LIBRARY_VERSION }}" />'
86+
Write-Host "Expecting: $expected"
87+
$found = Select-String -Path $path -Pattern $expected -SimpleMatch -Quiet
88+
if (-not $found) {
89+
Write-Error "Package reference not found: $expected"
90+
exit 1
91+
}
92+
Write-Host "Package reference found."
93+
94+
- name: Create MIDI ports (Windows)
95+
if: runner.os == 'Windows'
96+
uses: ./.github/actions/create-ports-windows
97+
with:
98+
enabled: 'true'
99+
ports-names: 'MIDI A,MIDI B,MIDI C'
100+
101+
- name: Create MIDI ports (macOS)
102+
if: runner.os == 'macOS'
103+
uses: ./.github/actions/create-ports-macos
104+
with:
105+
enabled: 'true'
106+
107+
- name: Run app via dotnet
108+
shell: pwsh
109+
run: |
110+
$path = Resolve-Path 'Resources/PackageIntegrationTestUtilities/DwmAotApp/DwmAotApp/DwmAotApp.csproj'
111+
dotnet run --project "$path" --configuration ${{ vars.BUILD_CONFIGURATION }}
112+
113+
- name: Run app after publish
114+
shell: pwsh
115+
run: |
116+
$projectPath = "Resources/PackageIntegrationTestUtilities/DwmAotApp/DwmAotApp/DwmAotApp.csproj"
117+
118+
foreach ($optref in @('Size', 'Speed')) {
119+
foreach ($gensttr in @('true', 'false')) {
120+
foreach ($invglob in @('true', 'false')) {
121+
foreach ($trimmet in @('true', 'false')) {
122+
$publishOptions = @"
123+
IlcOptimizationPreference = $optref
124+
IlcGenerateStackTraceData = $gensttr
125+
InvariantGlobalization = $invglob
126+
IlcTrimMetadata = $trimmet
127+
"@
128+
Write-Host "$publishOptions"
129+
130+
$outputPath = "${{ runner.temp }}/publish/$rid/optref-$optref-gensttr-$gensttr-invglob-$invglob-trimmet-$trimmet"
131+
Write-Host "Output path set to '$outputPath'"
132+
133+
Write-Host "Publishing the app..."
134+
dotnet publish `
135+
--configuration ${{ vars.BUILD_CONFIGURATION }} `
136+
--runtime ${{ matrix.rid }} `
137+
-p:IlcOptimizationPreference=$optref `
138+
-p:IlcGenerateStackTraceData=$gensttr `
139+
-p:InvariantGlobalization=$invglob `
140+
-p:IlcTrimMetadata=$trimmet `
141+
-p:SolutionDir="${{ github.workspace }}/" `
142+
--output $outputPath `
143+
$projectPath
144+
145+
if ($LASTEXITCODE -ne 0) {
146+
Write-Error "Publish failed with exit code $LASTEXITCODE"
147+
exit $LASTEXITCODE
148+
}
149+
150+
Write-Host "Listing published files..."
151+
Write-Host ""
152+
Get-ChildItem -Recurse -Force -Path "$outputPath" | Format-Table @{Label="File Name"; Expression={$_.Name}}, @{Label="Size (KB)"; Expression={"{0:N2}" -f ($_.Length / 1KB)}}
153+
Write-Host ""
154+
155+
Write-Host "Running the app..."
156+
Write-Host ""
157+
if ($IsWindows) {
158+
& "$outputPath/DwmAotApp.exe"
159+
} else {
160+
& "$outputPath/DwmAotApp"
161+
}
162+
163+
if ($LASTEXITCODE -ne 0) {
164+
Write-Error "App execution failed with exit code $LASTEXITCODE"
165+
exit $LASTEXITCODE
166+
}
167+
168+
Write-Host ""
169+
Write-Host "Configuration test completed successfully"
170+
Write-Host ""
171+
}
172+
}
173+
}
174+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#if !NET5_0_OR_GREATER
2+
3+
namespace System.Diagnostics.CodeAnalysis
4+
{
5+
/// <summary>
6+
/// Indicates that certain members on a specified <see cref="Type"/> are accessed dynamically,
7+
/// for example through <see cref="System.Reflection"/>.
8+
/// </summary>
9+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, Inherited = false)]
10+
internal sealed class DynamicallyAccessedMembersAttribute : Attribute
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="DynamicallyAccessedMembersAttribute"/> class
14+
/// with the specified member types.
15+
/// </summary>
16+
/// <param name="memberTypes">The types of members dynamically accessed.</param>
17+
public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)
18+
{
19+
MemberTypes = memberTypes;
20+
}
21+
22+
/// <summary>
23+
/// Gets the <see cref="DynamicallyAccessedMemberTypes"/> which specifies the type
24+
/// of members dynamically accessed.
25+
/// </summary>
26+
public DynamicallyAccessedMemberTypes MemberTypes { get; }
27+
}
28+
29+
/// <summary>
30+
/// Specifies the types of members that are dynamically accessed.
31+
/// </summary>
32+
[Flags]
33+
internal enum DynamicallyAccessedMemberTypes
34+
{
35+
None = 0,
36+
PublicParameterlessConstructor = 1,
37+
PublicConstructors = 3,
38+
NonPublicConstructors = 4,
39+
PublicMethods = 8,
40+
NonPublicMethods = 16,
41+
PublicFields = 32,
42+
NonPublicFields = 64,
43+
PublicNestedTypes = 128,
44+
NonPublicNestedTypes = 256,
45+
PublicProperties = 512,
46+
NonPublicProperties = 1024,
47+
PublicEvents = 2048,
48+
NonPublicEvents = 4096,
49+
Interfaces = 8192,
50+
All = -1
51+
}
52+
}
53+
54+
#endif
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#if !NET5_0_OR_GREATER
2+
3+
namespace System.Diagnostics.CodeAnalysis
4+
{
5+
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
6+
internal sealed class UnconditionalSuppressMessageAttribute : Attribute
7+
{
8+
public UnconditionalSuppressMessageAttribute(string category, string checkId)
9+
{
10+
Category = category;
11+
CheckId = checkId;
12+
}
13+
14+
public string Category { get; }
15+
16+
public string CheckId { get; }
17+
18+
public string? Scope { get; set; }
19+
20+
public string? Target { get; set; }
21+
22+
public string? MessageId { get; set; }
23+
24+
public string? Justification { get; set; }
25+
}
26+
}
27+
28+
#endif

DryWetMidi/Common/EnumHelper.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
3+
namespace Melanchall.DryWetMidi.Common
4+
{
5+
internal static class EnumHelper
6+
{
7+
public static T[] GetValues<T>() where T : struct, Enum
8+
{
9+
#if NET5_0_OR_GREATER
10+
return Enum.GetValues<T>();
11+
#else
12+
return (T[])Enum.GetValues(typeof(T));
13+
#endif
14+
}
15+
}
16+
}

DryWetMidi/Composing/Actions/AddTextEventAction.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using Melanchall.DryWetMidi.Core;
34
using Melanchall.DryWetMidi.Interaction;
45

56
namespace Melanchall.DryWetMidi.Composing
67
{
7-
internal sealed class AddTextEventAction<TEvent> : PatternAction
8+
internal sealed class AddTextEventAction<
9+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TEvent> : PatternAction
810
where TEvent : BaseTextEvent
911
{
1012
#region Constructor

DryWetMidi/Core/Chunks/Info/ChunkTypesCollection.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.Linq;
56

67
namespace Melanchall.DryWetMidi.Core
@@ -37,7 +38,7 @@ public sealed class ChunkTypesCollection : IEnumerable<ChunkType>
3738
/// </exception>
3839
/// <exception cref="ArgumentException">Chunk type specified by <paramref name="type"/> and
3940
/// <paramref name="id"/> already exists in the <see cref="ChunksCollection"/>.</exception>
40-
public void Add(Type type, string id)
41+
public void Add([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type, string id)
4142
{
4243
_ids.Add(type, id);
4344
_types.Add(id, type);
@@ -52,7 +53,8 @@ public void Add(Type type, string id)
5253
/// uninitialized.</param>
5354
/// <returns><c>true</c> if the <see cref="ChunkTypesCollection"/> contains a chunk type with the
5455
/// specified ID; otherwise, <c>false</c>.</returns>
55-
public bool TryGetType(string id, out Type type)
56+
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "All types stored in this collection are guaranteed to have public parameterless constructors via the Add method's annotation.")]
57+
public bool TryGetType(string id, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] out Type type)
5658
{
5759
return _types.TryGetValue(id, out type);
5860
}
@@ -65,7 +67,7 @@ public bool TryGetType(string id, out Type type)
6567
/// chunk type, if the type is found; otherwise, <c>null</c>. This parameter is passed uninitialized.</param>
6668
/// <returns><c>true</c> if the <see cref="ChunkTypesCollection"/> contains an ID for the
6769
/// specified chunk type; otherwise, <c>false</c>.</returns>
68-
public bool TryGetId(Type type, out string id)
70+
public bool TryGetId([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type, out string id)
6971
{
7072
return _ids.TryGetValue(type, out id);
7173
}

DryWetMidi/Core/Events/Info/EventTypesCollection.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.Linq;
56

67
namespace Melanchall.DryWetMidi.Core
@@ -27,7 +28,7 @@ public sealed class EventTypesCollection : IEnumerable<EventType>
2728
/// <exception cref="ArgumentNullException"><paramref name="type"/> is <c>null</c>.</exception>
2829
/// <exception cref="ArgumentException">Event type specified by <paramref name="type"/> and
2930
/// <paramref name="statusByte"/> already exists in the <see cref="EventsCollection"/>.</exception>
30-
public void Add(Type type, byte statusByte)
31+
public void Add([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type, byte statusByte)
3132
{
3233
_statusBytes.Add(type, statusByte);
3334
_types.Add(statusByte, type);
@@ -42,7 +43,8 @@ public void Add(Type type, byte statusByte)
4243
/// is passed uninitialized.</param>
4344
/// <returns><c>true</c> if the <see cref="EventTypesCollection"/> contains an event type with the
4445
/// specified status byte; otherwise, <c>false</c>.</returns>
45-
public bool TryGetType(byte statusByte, out Type type)
46+
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "All types stored in this collection are guaranteed to have public parameterless constructors via the Add method's annotation.")]
47+
public bool TryGetType(byte statusByte, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] out Type type)
4648
{
4749
return _types.TryGetValue(statusByte, out type);
4850
}
@@ -56,7 +58,7 @@ public bool TryGetType(byte statusByte, out Type type)
5658
/// uninitialized.</param>
5759
/// <returns><c>true</c> if the <see cref="EventTypesCollection"/> contains a status byte for the
5860
/// specified event type; otherwise, <c>false</c>.</returns>
59-
public bool TryGetStatusByte(Type type, out byte statusByte)
61+
public bool TryGetStatusByte([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type, out byte statusByte)
6062
{
6163
return _statusBytes.TryGetValue(type, out statusByte);
6264
}

DryWetMidi/Core/Events/Readers/MetaEventReader.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23

34
namespace Melanchall.DryWetMidi.Core
45
{
@@ -98,7 +99,7 @@ public MidiEvent Read(MidiReader reader, ReadingSettings settings, byte currentS
9899

99100
#region Methods
100101

101-
private static bool IsMetaEventType(Type type)
102+
private static bool IsMetaEventType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
102103
{
103104
return type != null &&
104105
type.IsSubclassOf(typeof(MetaEvent)) &&

DryWetMidi/Core/Events/Writers/MetaEventWriter.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
using System.Diagnostics;
2-
using Melanchall.DryWetMidi.Common;
1+
using Melanchall.DryWetMidi.Common;
2+
using System.Diagnostics;
3+
using System.Diagnostics.CodeAnalysis;
34

45
namespace Melanchall.DryWetMidi.Core
56
{
67
internal sealed class MetaEventWriter : IEventWriter
78
{
89
#region IEventWriter
910

11+
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Custom meta event types are guaranteed to have public parameterless constructors when registered via CustomMetaEventTypes.Add.")]
1012
public void Write(MidiEvent midiEvent, MidiWriter writer, WritingSettings settings, bool writeStatusByte)
1113
{
1214
if (writeStatusByte)

0 commit comments

Comments
 (0)