diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 9ebe527..e1c6038 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,6 +19,10 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v6 with: - dotnet-version: 6.0.x + dotnet-version: | + 6.0.x + 10.0.x - name: Run CI run: dotnet fsi src/build.fsx + - name: Test NativeAOT package consumer + run: dotnet fsi src/build.fsx -- -p NativeAOT diff --git a/.gitignore b/.gitignore index a394bdb..29a3321 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,8 @@ nupkg _UpgradeReport_Files/ [Pp]ackages/ +.vscode/ + Thumbs.db Desktop.ini .DS_Store diff --git a/src/FSharp.SystemCommandLine/FSharp.SystemCommandLine.TrimmerRoots.xml b/src/FSharp.SystemCommandLine/FSharp.SystemCommandLine.TrimmerRoots.xml new file mode 100644 index 0000000..c72fc92 --- /dev/null +++ b/src/FSharp.SystemCommandLine/FSharp.SystemCommandLine.TrimmerRoots.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/FSharp.SystemCommandLine/FSharp.SystemCommandLine.fsproj b/src/FSharp.SystemCommandLine/FSharp.SystemCommandLine.fsproj index 7f60725..55fe1c7 100644 --- a/src/FSharp.SystemCommandLine/FSharp.SystemCommandLine.fsproj +++ b/src/FSharp.SystemCommandLine/FSharp.SystemCommandLine.fsproj @@ -19,6 +19,11 @@ + + + + diff --git a/src/TestConsole/TestConsole.fsproj b/src/TestConsole/TestConsole.fsproj index e6b7ef0..141fe43 100644 --- a/src/TestConsole/TestConsole.fsproj +++ b/src/TestConsole/TestConsole.fsproj @@ -2,7 +2,7 @@ Exe - net6.0 + net6.0;net10.0 diff --git a/src/TestNativeAot/Program.fs b/src/TestNativeAot/Program.fs new file mode 100644 index 0000000..0651a3d --- /dev/null +++ b/src/TestNativeAot/Program.fs @@ -0,0 +1,18 @@ +open System +open System.Runtime.CompilerServices +open FSharp.SystemCommandLine +open Input + +[] +let main argv = + if RuntimeFeature.IsDynamicCodeSupported then + failwith "This smoke test must run as a NativeAOT executable." + + rootCommand argv { + description "NativeAOT startup and scalar/array parsing regression test" + inputs (argument "words" |> arity ZeroOrMore, option "--count") + setAction (fun (words, count) -> + // Avoid printf's own reflection requirements so this tests the library's input path. + Console.WriteLine("words=" + String.Join(",", words) + ";count=" + count.ToString()) + ) + } diff --git a/src/TestNativeAot/TestNativeAot.fsproj b/src/TestNativeAot/TestNativeAot.fsproj new file mode 100644 index 0000000..0d8375b --- /dev/null +++ b/src/TestNativeAot/TestNativeAot.fsproj @@ -0,0 +1,15 @@ + + + Exe + net10.0 + true + false + $(MSBuildThisFileDirectory)obj/packages + + + + + + + + diff --git a/src/TestNativeAot/global.json b/src/TestNativeAot/global.json new file mode 100644 index 0000000..d46d21e --- /dev/null +++ b/src/TestNativeAot/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/src/build.fsx b/src/build.fsx index 460f9f5..c6b4f24 100644 --- a/src/build.fsx +++ b/src/build.fsx @@ -1,6 +1,10 @@ #r "nuget: Fun.Build, 1.1.17" open Fun.Build +open System +open System.Diagnostics +open System.IO +open System.Runtime.InteropServices let src = __SOURCE_DIRECTORY__ let fsproj = $"{src}/FSharp.SystemCommandLine/FSharp.SystemCommandLine.fsproj" @@ -19,6 +23,57 @@ pipeline "Build" { runIfOnlySpecified false } +// Use a unique package version so repeated local runs cannot reuse a cached build. +let smokeDir = Path.Combine(src, "TestNativeAot") +let smokePackages = Path.Combine(smokeDir, "obj", "packages") +let smokeOutput = Path.Combine(smokeDir, "bin", "native-aot") +let smokeVersion = "0.0.0-aot-" + Guid.NewGuid().ToString("N") +let smokeExecutable = + Path.Combine(smokeOutput, if OperatingSystem.IsWindows() then "TestNativeAot.exe" else "TestNativeAot") + +let checkNativeAot args expectedExitCode expectedOutput = + let startInfo = ProcessStartInfo(smokeExecutable) + startInfo.UseShellExecute <- false + startInfo.RedirectStandardOutput <- true + startInfo.RedirectStandardError <- true + for arg in args do startInfo.ArgumentList.Add(arg) + use child = Process.Start(startInfo) + let stdout = child.StandardOutput.ReadToEndAsync() + let stderr = child.StandardError.ReadToEndAsync() + if not (child.WaitForExit(30_000)) then + child.Kill(true) + failwithf "NativeAOT smoke test timed out: %A" args + let output = stdout.GetAwaiter().GetResult() + let errors = stderr.GetAwaiter().GetResult() + if child.ExitCode <> expectedExitCode || not (output.Contains(expectedOutput: string)) then + failwithf "NativeAOT smoke test %A: expected exit %d and output containing %A; got exit %d.\n%s\n%s" + args expectedExitCode expectedOutput child.ExitCode output errors + printfn "NativeAOT smoke test passed: %A" args + +pipeline "NativeAOT" { + // The consumer's global.json selects .NET 10 for both pack and publish. + workingDir smokeDir + + stage "Pack library for NativeAOT" { + run $"dotnet pack \"{fsproj}\" --configuration Release --output \"{smokePackages}\" -p:PackageVersion={smokeVersion}" + } + + stage "Publish NativeAOT package consumer" { + run $"dotnet publish TestNativeAot.fsproj --configuration Release --runtime {RuntimeInformation.RuntimeIdentifier} --output \"{smokeOutput}\" -p:SmokePackageVersion={smokeVersion}" + } + + stage "Run NativeAOT smoke tests" { + run (fun _ -> + checkNativeAot [ "--help" ] 0 "--count" + checkNativeAot [ "hello"; "world"; "--count"; "2" ] 0 "words=hello,world;count=2" + checkNativeAot [] 0 "words=;count=0" + checkNativeAot [ "--count"; "invalid" ] 1 "" + ) + } + + runIfOnlySpecified true +} + open System.Xml.Linq pipeline "Publish" { @@ -39,4 +94,4 @@ pipeline "Publish" { runIfOnlySpecified true } -tryPrintPipelineCommandHelp () \ No newline at end of file +tryPrintPipelineCommandHelp ()