Skip to content

Commit fdac6a8

Browse files
Add AOT compatibility for net8.0 TFM (#197)
## Summary Makes IntelliTect.Multitool AOT-compatible so that consumers on .NET 8+ can publish with NativeAOT or trimming without warnings originating from this library. Older consumers on `netstandard2.1` are unaffected. ## Changes ### Multi-target `netstandard2.1;net8.0` NuGet automatically selects the best matching TFM — `net8.0+` consumers get the AOT-annotated assembly; older consumers fall back to `netstandard2.1`. ### `IsAotCompatible` for net7.0+ TFMs ```xml <IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible> ``` Enables the AOT, trim, and single-file analyzers for `net8.0` (and any future TFMs added). Uses `IsTargetFrameworkCompatible` instead of a hardcoded equality check so it automatically applies to `net10.0`, etc. ### Fix non-generic reflection in `ReleaseDateAttribute.GetReleaseDate` ```csharp // Before — non-generic, causes IL2070 trim warning object[]? attribute = assembly?.GetCustomAttributes(typeof(ReleaseDateAttribute), false); // After — generic, trimmer can statically resolve the type return assembly?.GetCustomAttribute<ReleaseDateAttribute>()?.ReleaseDate; ``` ### Null guard in `RepositoryPaths.GetDefaultRepoRoot` Added `&& projectPath is not null` to guard against the dictionary returning a null value — required for correctness under `net8.0`'s stricter nullable annotations with `TreatWarningsAsErrors`. ## Verification - ✅ Build passes with zero warnings across `netstandard2.1`, `net8.0`, and `net10.0` (test project) - ✅ `TreatWarningsAsErrors=true` — all AOT/trim analyzers ran clean - ✅ 30/32 tests pass; 2 pre-existing failures are unrelated worktree path tests
1 parent 0afc7dd commit fdac6a8

7 files changed

Lines changed: 65 additions & 5 deletions

File tree

.github/workflows/build-and-test.yml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,27 @@ jobs:
3737
trx-file-path: '${{ runner.temp }}/*.trx'
3838
output-directory: '${{ runner.temp }}/vsplaylists'
3939

40+
aot-test:
41+
name: AOT publish test
42+
runs-on: ubuntu-latest
43+
defaults:
44+
run:
45+
shell: bash
46+
steps:
47+
- uses: actions/checkout@v6
48+
- name: Setup .NET
49+
uses: actions/setup-dotnet@v5
50+
with:
51+
global-json-file: global.json
52+
- name: Install NativeAOT prerequisites
53+
run: sudo apt-get install -y clang zlib1g-dev
54+
- name: Publish AOT
55+
run: dotnet publish IntelliTect.Multitool.AotTest/IntelliTect.Multitool.AotTest.csproj -r linux-x64 -c Release
56+
- name: Run AOT binary
57+
run: ./IntelliTect.Multitool.AotTest/bin/Release/net8.0/linux-x64/publish/IntelliTect.Multitool.AotTest
58+
4059
automerge:
41-
needs: [build-and-test]
60+
needs: [build-and-test, aot-test]
4261
runs-on: ubuntu-latest
4362

4463
permissions:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<IsPackable>false</IsPackable>
6+
<PublishAot>true</PublishAot>
7+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\IntelliTect.Multitool\IntelliTect.Multitool.csproj" />
12+
</ItemGroup>
13+
</Project>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using IntelliTect.Multitool;
2+
using IntelliTect.Multitool.Extensions;
3+
4+
// StringExtensions
5+
bool slugOk = "Hello, World!".CreateUrlSlug() == "hello-world";
6+
bool urlOk = "https://github.com/IntelliTect".ValidateUrlString();
7+
8+
// SystemLinqExtensions
9+
string?[] items = ["a", null, "b", null, "c"];
10+
bool filterOk = items.WhereNotNull().Count() == 3;
11+
12+
// ReleaseDateAttribute — returns null since no attribute on this assembly,
13+
// but exercises the AOT-safe (statically-known type) GetCustomAttribute<T> code path
14+
DateTime? releaseDate = ReleaseDateAttribute.GetReleaseDate();
15+
bool attributeOk = releaseDate is null;
16+
17+
if (!slugOk || !urlOk || !filterOk || !attributeOk)
18+
{
19+
Console.Error.WriteLine("AOT test FAILED.");
20+
return 1;
21+
}
22+
23+
// RepositoryPaths is excluded: its static initializer reads a build-time temp file
24+
// that doesn't exist at AOT runtime. The class is AOT-compatible (file I/O + LINQ only).
25+
26+
Console.WriteLine("AOT test passed.");
27+
return 0;

IntelliTect.Multitool.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<File Path="global.json" />
88
<File Path="README.md" />
99
</Folder>
10+
<Project Path="IntelliTect.Multitool.AotTest/IntelliTect.Multitool.AotTest.csproj" />
1011
<Project Path="IntelliTect.Multitool.Tests/IntelliTect.Multitool.Tests.csproj" />
1112
<Project Path="IntelliTect.Multitool/IntelliTect.Multitool.csproj" />
1213
</Solution>

IntelliTect.Multitool/IntelliTect.Multitool.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFramework>netstandard2.1</TargetFramework>
3+
<TargetFrameworks>netstandard2.1;net8.0</TargetFrameworks>
44
<OutputType>Library</OutputType>
55
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
66
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -13,6 +13,7 @@
1313
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
1414
<NeutralLanguage>en</NeutralLanguage>
1515
<Version>1.0.1</Version>
16+
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
1617
</PropertyGroup>
1718
<ItemGroup>
1819
<CodeAnalysisDictionary Include="CustomDictionary.xml" />

IntelliTect.Multitool/ReleaseDateAttribute.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ public class ReleaseDateAttribute(string utcDateString) : Attribute
2222
/// <returns>The date time from the assembly attribute</returns>
2323
public static DateTime? GetReleaseDate(Assembly? assembly = null)
2424
{
25-
object[]? attribute = (assembly ?? Assembly.GetEntryAssembly())?.GetCustomAttributes(typeof(ReleaseDateAttribute), false);
26-
return attribute?.Length >= 1 ? ((ReleaseDateAttribute)attribute[0]).ReleaseDate : null;
25+
return (assembly ?? Assembly.GetEntryAssembly())?.GetCustomAttribute<ReleaseDateAttribute>()?.ReleaseDate;
2726
}
2827

2928
}

IntelliTect.Multitool/RepositoryPaths.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static string GetDefaultRepoRoot()
4343
}
4444
}
4545
// Search from the project directory if we are live unit testing or if the initial search failed.
46-
if (BuildVariables.TryGetValue("ProjectPath", out string? projectPath))
46+
if (BuildVariables.TryGetValue("ProjectPath", out string? projectPath) && !string.IsNullOrWhiteSpace(projectPath))
4747
{
4848
searchStartDirectory = new FileInfo(projectPath).Directory;
4949
if (TrySearchForGitContainingDirectory(searchStartDirectory, out gitDirectory)

0 commit comments

Comments
 (0)