diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 4841fd9..9428203 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -14,15 +14,19 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-latest ] + os: [ ubuntu-latest, windows-latest ] steps: - name: Checkout uses: actions/checkout@v5 + - name: Setup .NET Core uses: actions/setup-dotnet@v4 + - name: Restore run: dotnet restore + - name: Build run: dotnet build -c Release --no-restore + - name: Test - run: dotnet test -c Release + run: dotnet test -c Release --no-build diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..81e9c3f --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,34 @@ +name: Test + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build -c Release --no-restore + + - name: Test + run: dotnet test -c Release --no-build + + - name: Pack + run: dotnet pack + + - name: Push to nuget.org + env: + VERSION: ${{ github.ref_name }} + run: dotnet nuget push "./artifacts/package/release/runfs.${VERSION}.nupkg" -k ${{ secrets.NUGET_KEY }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..98c0711 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# CHANGELOG + +## 1.0.3 + +### changed + +* Direct calls to msbuild, making runfs faster. But still no virtual (in-memory) project file due to sdk dll hell. + +## 1.0.2 + +### added + +* Initial version diff --git a/Directory.Build.Props b/Directory.Build.props similarity index 100% rename from Directory.Build.Props rename to Directory.Build.props diff --git a/global.json b/global.json index d599efa..9334ba5 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,7 @@ { "sdk": { - "version": "9.0.304" + "version": "9.0.304", + "rollForward": "latestMinor", + "allowPrerelease": true } } \ No newline at end of file diff --git a/src/Runfs/Build.fs b/src/Runfs/Build.fs new file mode 100644 index 0000000..98e9d18 --- /dev/null +++ b/src/Runfs/Build.fs @@ -0,0 +1,71 @@ +module Runfs.Build + +open Microsoft.Build.Construction +open Microsoft.Build.Definition +open Microsoft.Build.Evaluation +open Microsoft.Build.Execution +open Microsoft.Build.Framework +open Microsoft.Build.Logging +open Microsoft.Build.Locator +open System +open System.IO +open System.Xml +open Runfs.ProjectFile + +type Project = + {buildManager: BuildManager; projectInstance: ProjectInstance} + interface IDisposable with + member this.Dispose() = + this.buildManager.EndBuild() + this.projectInstance.FullPath |> File.Delete + +type MSBuildError = MSBuildError of target: string * result: string + +let initMSBuild() = MSBuildLocator.RegisterDefaults() |> ignore + +let createProject verbose projectFilePath (projectFileText: string) : Project = + let verbosity = if verbose then "m" else "q" + let loggerArgs = [|$"-verbosity:{verbosity}"; "-tl:off"; "NoSummary"|] + let consoleLogger = TerminalLogger.CreateTerminalOrConsoleLogger loggerArgs + let loggers = [|consoleLogger|] + let globalProperties = + dict [ + ] + let projectCollection = new ProjectCollection( + globalProperties, + loggers, + ToolsetDefinitionLocations.Default) + let options = ProjectOptions() + options.ProjectCollection <- projectCollection + options.GlobalProperties <- globalProperties + + // let reader = new StringReader(projectFileText) + // let xmlReader = XmlReader.Create reader + // let projectRoot = ProjectRootElement.Create(xmlReader, projectCollection) + // projectRoot.FullPath <- projectFilePath + // let projectInstance = ProjectInstance.FromProjectRootElement(projectRoot, options) + + File.WriteAllText(projectFilePath, projectFileText) + let projectInstance = ProjectInstance.FromFile(projectFilePath, options) + + let parameters = BuildParameters projectCollection + parameters.Loggers <- loggers + parameters.LogTaskInputs <- false + let buildManager = BuildManager.DefaultBuildManager + buildManager.BeginBuild parameters + {buildManager = buildManager; projectInstance = projectInstance} + +let build target project = + let flags = + BuildRequestDataFlags.ClearCachesAfterBuild + ||| BuildRequestDataFlags.SkipNonexistentTargets + ||| BuildRequestDataFlags.IgnoreMissingEmptyAndInvalidImports + ||| BuildRequestDataFlags.FailOnUnresolvedSdk + let buildRequest = + new BuildRequestData(project.projectInstance, [|target|], null, flags) + + let buildResult = project.buildManager.BuildRequest buildRequest + if buildResult.OverallResult = BuildResultCode.Success then + Ok() + else + Error(MSBuildError(target, string buildResult)) diff --git a/src/Runfs/Dependencies.fs b/src/Runfs/Dependencies.fs index dd47cb7..e1f8a04 100644 --- a/src/Runfs/Dependencies.fs +++ b/src/Runfs/Dependencies.fs @@ -8,7 +8,7 @@ open Runfs.Directives open Runfs.Utilities let RuntimeVersion = Environment.Version -let SdkVersion = Runtime.InteropServices.RuntimeInformation.FrameworkDescription +let SdkVersion = "t.b.d" // TODO (needed?) let TargetFramework = $"net{RuntimeVersion.Major}.{RuntimeVersion.Minor}" let PotentialImplicitBuildFileNames = [ diff --git a/src/Runfs/Program.fs b/src/Runfs/Program.fs index 72e5d64..2723168 100644 --- a/src/Runfs/Program.fs +++ b/src/Runfs/Program.fs @@ -2,6 +2,7 @@ open Runfs.Runfs open Runfs.Directives +open Runfs.Build [] let main argv = @@ -37,10 +38,7 @@ let main argv = | CaughtException ex -> [$"Unexpected: {ex.Message}"] | InvalidSourcePath s -> [$"Invalid source path: {s}"] | InvalidSourceDirectory s -> [$"Invalid source directory: {s}"] - | RestoreError(stdoutLines, stderrLines) -> - "Restore error" :: indent stdoutLines @ indent stderrLines - | BuildError(stdoutLines, stderrLines) -> - "Build error" :: indent stdoutLines @ indent stderrLines + | BuildError(MSBuildError(target, result)) -> [$"MSBuild {target} error: {result}"] | DirectiveError parseErrors -> let getParseErrorString parseError = let prefix n = $" Line %3d{n}: " diff --git a/src/Runfs/ProjectFile.fs b/src/Runfs/ProjectFile.fs index c97453f..8a9a415 100644 --- a/src/Runfs/ProjectFile.fs +++ b/src/Runfs/ProjectFile.fs @@ -16,8 +16,8 @@ let private escape str = SecurityElement.Escape str |> string let private sdkLine project (name, version) = match version with - | Some v -> $""" """ - | None -> $""" """ + | Some v -> $""" """ + | None -> $""" """ let private propertyLine (name, version) = $""" <{name}>{escape version}""" @@ -30,7 +30,7 @@ let private packageLine (name, version) = let createProjectFileLines directives entryPointSourceFullPath artifactsPath assemblyName = let sdks = match directives |> List.choose (function Sdk(n, v) -> Some(n, v) | _ -> None) with - | [] -> ["Microsoft.NET.Sdk", None] + | [] -> ["Microsoft.NET.Sdk", None] //DODO verison? | d -> d let properties = directives |> List.choose (function Property(n, v) -> Some(n.ToLowerInvariant(), v) | _ -> None) |> Map @@ -41,10 +41,11 @@ let createProjectFileLines directives entryPointSourceFullPath artifactsPath ass [ "" " " - $""" {assemblyName}""" + $""" {escape assemblyName}""" " true" " false" $""" {escape artifactsPath}""" + $""" true""" " " yield! sdks |> List.map (sdkLine "Sdk.props") " " diff --git a/src/Runfs/Runfs.fs b/src/Runfs/Runfs.fs index d0256cb..dffb70e 100644 --- a/src/Runfs/Runfs.fs +++ b/src/Runfs/Runfs.fs @@ -8,14 +8,14 @@ open Runfs.Directives open Runfs.ProjectFile open Runfs.Dependencies open Runfs.Utilities +open Runfs.Build type RunfsError = | CaughtException of Exception | InvalidSourcePath of string | InvalidSourceDirectory of string | DirectiveError of ParseError list - | RestoreError of stdout: string list * stderr: string list - | BuildError of stdout: string list * stderr: string list + | BuildError of MSBuildError let ThisPackageName = "Runfs" let DependenciesHashFileName = "dependencies.hash" @@ -58,11 +58,12 @@ let run (options, sourcePath, args) = let showTimings = Set.contains "time" options let verbose = Set.contains "verbose" options let noDependencyCheck = Set.contains "no-dependency-check" options - let withOutput = Set.contains "with-output" options let inline guardAndTime name f = guardAndTime showTimings name f + initMSBuild() + result { - let! fullSourcePath, fullSourceDir, artifactsDir, projectFilePath, + let! fullSourcePath, fullSourceDir, artifactsDir, virtualProjectFilePath, savedProjectFilePath, dependenciesHashPath, sourceHashPath, dllPath = guardAndTime "creating paths" <| fun () -> result { do! File.Exists sourcePath |> Result.requireTrue (InvalidSourcePath sourcePath) @@ -106,7 +107,7 @@ let run (options, sourcePath, args) = return computeDependenciesHash (string fullSourceDir) directives } - let! dependenciesChanged, sourceChanged, noDll = guardAndTime "computing build level" <| fun () -> + let! dependenciesChanged, sourceChanged, noExecutable = guardAndTime "computing build level" <| fun () -> let dependenciesChanged = if noDependencyCheck then false @@ -119,55 +120,37 @@ let run (options, sourcePath, args) = let noDll = not (File.Exists dllPath) Ok (dependenciesChanged, sourceChanged, noDll) - if dependenciesChanged || sourceChanged || noDll then + if dependenciesChanged || noExecutable then do! guardAndTime "creating and writing project file" <| fun () -> let projectFileLines = createProjectFileLines directives fullSourcePath artifactsDir AssemblyName File.WriteAllLines(savedProjectFilePath, projectFileLines) |> Ok + + if dependenciesChanged || sourceChanged || noExecutable then + use! project = guardAndTime "creating msbuild project instance" <| fun () -> + let projectFileText = File.ReadAllText savedProjectFilePath + createProject verbose virtualProjectFilePath projectFileText |> Ok + + if dependenciesChanged || noExecutable then + do! guardAndTime "running msbuild restore" <| fun () -> result { + File.Delete dependenciesHashPath + do! build "restore" project |> Result.mapError BuildError + } - if dependenciesChanged || noDll then - do! guardAndTime "running dotnet restore" <| fun () -> - File.Delete dependenciesHashPath - if File.Exists projectFilePath then File.Delete projectFilePath - File.Copy(savedProjectFilePath, projectFilePath) - let args = [ - "restore" - if not verbose then "-v:q" - projectFilePath - ] - let exitCode, stdoutLines, stderrLines = - runCommandCollectOutput "dotnet" args fullSourceDir - File.Delete projectFilePath - if exitCode <> 0 then Error(RestoreError(stdoutLines, stderrLines)) else Ok() - - if sourceChanged || dependenciesChanged || noDll then - do! guardAndTime "running dotnet build" <| fun () -> - if File.Exists projectFilePath then File.Delete projectFilePath - File.Copy(savedProjectFilePath, projectFilePath) - let args = [ - "build" - "--no-restore" - "-consoleLoggerParameters:NoSummary" - if not verbose then "-v:q" - projectFilePath - ] - let exitCode, stdoutLines, stderrLines = - runCommandCollectOutput "dotnet" args fullSourceDir - File.Delete projectFilePath - if exitCode <> 0 then Error(BuildError(stdoutLines, stderrLines)) else Ok() - - if dependenciesChanged then - do! guardAndTime "saving dependencies hash" <| fun () -> - File.WriteAllText(dependenciesHashPath, dependenciesHash) |> Ok - - if sourceChanged then - do! guardAndTime "saving source hash" <| fun () -> - File.WriteAllText(sourceHashPath, sourceHash) |> Ok + do! guardAndTime "running dotnet build" <| fun () -> result { + File.Delete sourceHash + do! build "build" project |> Result.mapError BuildError + } + + if dependenciesChanged then + do! guardAndTime "saving dependencies hash" <| fun () -> + File.WriteAllText(dependenciesHashPath, dependenciesHash) |> Ok + + if sourceChanged then + do! guardAndTime "saving source hash" <| fun () -> + File.WriteAllText(sourceHashPath, sourceHash) |> Ok let! exitCode = guardAndTime "executing program" <| fun () -> - if withOutput then - runCommandCollectOutput "dotnet" (dllPath::args) "." |> Ok - else - runCommand "dotnet" (dllPath::args) "." |> Ok + runCommand "dotnet" (dllPath::args) "." |> Ok return exitCode } diff --git a/src/Runfs/Runfs.fsproj b/src/Runfs/Runfs.fsproj index 97cdbd4..9fcbad1 100644 --- a/src/Runfs/Runfs.fsproj +++ b/src/Runfs/Runfs.fsproj @@ -2,7 +2,7 @@ Runfs - 1.0.2 + 1.0.3 "dotnet run app.cs" functionality for F#. Copyright 2025 by Martin521 Martin521 and contributors @@ -14,7 +14,7 @@ F# True runfs - + https://github.com/Martin521/Runfs/CHANGELOG.md true @@ -29,13 +29,18 @@ - + - + + + \ No newline at end of file diff --git a/tests/Runfs.Tests/ScriptTests.fs b/tests/Runfs.Tests/ScriptTests.fs index 59292db..3026e31 100644 --- a/tests/Runfs.Tests/ScriptTests.fs +++ b/tests/Runfs.Tests/ScriptTests.fs @@ -1,29 +1,38 @@ -module Runfs.Test.Simple +module Runfs.Test.ScriptTests open System.IO open Xunit +open Runfs.Utilities open Runfs.Runfs let thisFileDirectory = __SOURCE_DIRECTORY__ let testFile1 = Path.Join(thisFileDirectory, "TestFiles/test1.fs") let testFile2 = Path.Join(thisFileDirectory, "TestFiles/test2.fs") +let configPath = + #if DEBUG + "debug" + #else + "release" + #endif +let runfsDll = Path.Join(thisFileDirectory, $"../../artifacts/bin/Runfs/{configPath}/Runfs.dll") + +[] +let ``found runfs executable`` () = + Assert.True(File.Exists runfsDll, $"not found: {runfsDll}") + [] let ``testFile1 runs correctly`` () = - match run (Set["with-output"], testFile1, ["a"; "b"]) with - | Error err -> Assert.Fail $"unexpected error {err}" - | Ok (exitCode, outLines, errLines) -> - Assert.Equal(0, exitCode) - Assert.True(("args: [a; b]" = List.exactlyOne outLines)) - Assert.Empty errLines + let exitCode, outLines, errLines = runCommandCollectOutput "dotnet" [runfsDll; testFile1; "a"; "b"] thisFileDirectory + Assert.Equal(0, exitCode) + Assert.Equal("args: [a; b]", List.exactlyOne outLines) + Assert.Empty errLines [] let ``testFile2 runs correctly`` () = - match run (Set["with-output"], testFile2, []) with - | Error err -> Assert.Fail $"unexpected error {err}" - | Ok (exitCode, outLines, errLines) -> - Assert.Equal(0, exitCode) - Assert.True(("""{"x":"Hello","y":"world!"}""" = List.exactlyOne outLines)) - Assert.Empty errLines + let exitCode, outLines, errLines = runCommandCollectOutput "dotnet" [runfsDll; testFile2] thisFileDirectory + Assert.Equal(0, exitCode) + Assert.Equal("""{"x":"Hello","y":"world!"}""", List.exactlyOne outLines) + Assert.Empty errLines \ No newline at end of file diff --git a/tests/Runfs.Tests/TestFiles/test1.fs b/tests/Runfs.Tests/TestFiles/test1.fs index aba7288..cac4644 100644 --- a/tests/Runfs.Tests/TestFiles/test1.fs +++ b/tests/Runfs.Tests/TestFiles/test1.fs @@ -1,10 +1,17 @@ -open System +module A = + open System #r_project "abc/xyz.fsproj" #r_dll "System.dll" #r_property "myprop=43" #r_property "TargetFramework=net9.0" +open System + let args = System.Environment.GetCommandLineArgs() |> Array.toList |> List.tail printfn $"args: {args}" +// let RuntimeVersion = Environment.Version +// printfn $"Runtime version: {RuntimeVersion}" + + diff --git a/tests/Runfs.Tests/TestFiles/test2.fs b/tests/Runfs.Tests/TestFiles/test2.fs index 2d3c6e7..58d3815 100644 --- a/tests/Runfs.Tests/TestFiles/test2.fs +++ b/tests/Runfs.Tests/TestFiles/test2.fs @@ -1,4 +1,5 @@ #r_package "FSharp.SystemTextJson@1.4.36" +//#r_sdk "Microsoft.Net.Sdk" open System.Text.Json open System.Text.Json.Serialization @@ -7,4 +8,3 @@ let options = JsonFSharpOptions.Default().ToJsonSerializerOptions() let s = JsonSerializer.Serialize({| x = "Hello"; y = "world!" |}, options) printfn $"%s{s}" -