Skip to content

Commit 1e5fe95

Browse files
authored
call msbuild API directly (#1)
This makes runfs faster. It does not yet include using a "virtual" (in-memory) project file.
1 parent c2cdc69 commit 1e5fe95

File tree

14 files changed

+206
-79
lines changed

14 files changed

+206
-79
lines changed

.github/workflows/main.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,19 @@ jobs:
1414
runs-on: ${{ matrix.os }}
1515
strategy:
1616
matrix:
17-
os: [ ubuntu-latest ]
17+
os: [ ubuntu-latest, windows-latest ]
1818
steps:
1919
- name: Checkout
2020
uses: actions/checkout@v5
21+
2122
- name: Setup .NET Core
2223
uses: actions/setup-dotnet@v4
24+
2325
- name: Restore
2426
run: dotnet restore
27+
2528
- name: Build
2629
run: dotnet build -c Release --no-restore
30+
2731
- name: Test
28-
run: dotnet test -c Release
32+
run: dotnet test -c Release --no-build

.github/workflows/release.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
tags:
6+
- "[0-9]+.[0-9]+.[0-9]+"
7+
8+
env:
9+
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
10+
11+
jobs:
12+
publish:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-dotnet@v4
18+
19+
- name: Restore dependencies
20+
run: dotnet restore
21+
22+
- name: Build
23+
run: dotnet build -c Release --no-restore
24+
25+
- name: Test
26+
run: dotnet test -c Release --no-build
27+
28+
- name: Pack
29+
run: dotnet pack
30+
31+
- name: Push to nuget.org
32+
env:
33+
VERSION: ${{ github.ref_name }}
34+
run: dotnet nuget push "./artifacts/package/release/runfs.${VERSION}.nupkg" -k ${{ secrets.NUGET_KEY }}

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# CHANGELOG
2+
3+
## 1.0.3
4+
5+
### changed
6+
7+
* Direct calls to msbuild, making runfs faster. But still no virtual (in-memory) project file due to sdk dll hell.
8+
9+
## 1.0.2
10+
11+
### added
12+
13+
* Initial version

global.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"sdk": {
3-
"version": "9.0.304"
3+
"version": "9.0.304",
4+
"rollForward": "latestMinor",
5+
"allowPrerelease": true
46
}
57
}

src/Runfs/Build.fs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
module Runfs.Build
2+
3+
open Microsoft.Build.Construction
4+
open Microsoft.Build.Definition
5+
open Microsoft.Build.Evaluation
6+
open Microsoft.Build.Execution
7+
open Microsoft.Build.Framework
8+
open Microsoft.Build.Logging
9+
open Microsoft.Build.Locator
10+
open System
11+
open System.IO
12+
open System.Xml
13+
open Runfs.ProjectFile
14+
15+
type Project =
16+
{buildManager: BuildManager; projectInstance: ProjectInstance}
17+
interface IDisposable with
18+
member this.Dispose() =
19+
this.buildManager.EndBuild()
20+
this.projectInstance.FullPath |> File.Delete
21+
22+
type MSBuildError = MSBuildError of target: string * result: string
23+
24+
let initMSBuild() = MSBuildLocator.RegisterDefaults() |> ignore
25+
26+
let createProject verbose projectFilePath (projectFileText: string) : Project =
27+
let verbosity = if verbose then "m" else "q"
28+
let loggerArgs = [|$"-verbosity:{verbosity}"; "-tl:off"; "NoSummary"|]
29+
let consoleLogger = TerminalLogger.CreateTerminalOrConsoleLogger loggerArgs
30+
let loggers = [|consoleLogger|]
31+
let globalProperties =
32+
dict [
33+
]
34+
let projectCollection = new ProjectCollection(
35+
globalProperties,
36+
loggers,
37+
ToolsetDefinitionLocations.Default)
38+
let options = ProjectOptions()
39+
options.ProjectCollection <- projectCollection
40+
options.GlobalProperties <- globalProperties
41+
42+
// let reader = new StringReader(projectFileText)
43+
// let xmlReader = XmlReader.Create reader
44+
// let projectRoot = ProjectRootElement.Create(xmlReader, projectCollection)
45+
// projectRoot.FullPath <- projectFilePath
46+
// let projectInstance = ProjectInstance.FromProjectRootElement(projectRoot, options)
47+
48+
File.WriteAllText(projectFilePath, projectFileText)
49+
let projectInstance = ProjectInstance.FromFile(projectFilePath, options)
50+
51+
let parameters = BuildParameters projectCollection
52+
parameters.Loggers <- loggers
53+
parameters.LogTaskInputs <- false
54+
let buildManager = BuildManager.DefaultBuildManager
55+
buildManager.BeginBuild parameters
56+
{buildManager = buildManager; projectInstance = projectInstance}
57+
58+
let build target project =
59+
let flags =
60+
BuildRequestDataFlags.ClearCachesAfterBuild
61+
||| BuildRequestDataFlags.SkipNonexistentTargets
62+
||| BuildRequestDataFlags.IgnoreMissingEmptyAndInvalidImports
63+
||| BuildRequestDataFlags.FailOnUnresolvedSdk
64+
let buildRequest =
65+
new BuildRequestData(project.projectInstance, [|target|], null, flags)
66+
67+
let buildResult = project.buildManager.BuildRequest buildRequest
68+
if buildResult.OverallResult = BuildResultCode.Success then
69+
Ok()
70+
else
71+
Error(MSBuildError(target, string buildResult))

src/Runfs/Dependencies.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ open Runfs.Directives
88
open Runfs.Utilities
99

1010
let RuntimeVersion = Environment.Version
11-
let SdkVersion = Runtime.InteropServices.RuntimeInformation.FrameworkDescription
11+
let SdkVersion = "t.b.d" // TODO (needed?)
1212
let TargetFramework = $"net{RuntimeVersion.Major}.{RuntimeVersion.Minor}"
1313

1414
let PotentialImplicitBuildFileNames = [

src/Runfs/Program.fs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
open Runfs.Runfs
44
open Runfs.Directives
5+
open Runfs.Build
56

67
[<EntryPoint>]
78
let main argv =
@@ -37,10 +38,7 @@ let main argv =
3738
| CaughtException ex -> [$"Unexpected: {ex.Message}"]
3839
| InvalidSourcePath s -> [$"Invalid source path: {s}"]
3940
| InvalidSourceDirectory s -> [$"Invalid source directory: {s}"]
40-
| RestoreError(stdoutLines, stderrLines) ->
41-
"Restore error" :: indent stdoutLines @ indent stderrLines
42-
| BuildError(stdoutLines, stderrLines) ->
43-
"Build error" :: indent stdoutLines @ indent stderrLines
41+
| BuildError(MSBuildError(target, result)) -> [$"MSBuild {target} error: {result}"]
4442
| DirectiveError parseErrors ->
4543
let getParseErrorString parseError =
4644
let prefix n = $" Line %3d{n}: "

src/Runfs/ProjectFile.fs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ let private escape str = SecurityElement.Escape str |> string
1616

1717
let private sdkLine project (name, version) =
1818
match version with
19-
| Some v -> $""" <Import Project="{project}" Sdk="{escape name}" Version="{escape v}" />"""
20-
| None -> $""" <Import Project="{project}" Sdk="{escape name}" />"""
19+
| Some v -> $""" <Import Project="{escape project}" Sdk="{escape name}" Version="{escape v}" />"""
20+
| None -> $""" <Import Project="{escape project}" Sdk="{escape name}" />"""
2121

2222
let private propertyLine (name, version) =
2323
$""" <{name}>{escape version}</{name}>"""
@@ -30,7 +30,7 @@ let private packageLine (name, version) =
3030
let createProjectFileLines directives entryPointSourceFullPath artifactsPath assemblyName =
3131
let sdks =
3232
match directives |> List.choose (function Sdk(n, v) -> Some(n, v) | _ -> None) with
33-
| [] -> ["Microsoft.NET.Sdk", None]
33+
| [] -> ["Microsoft.NET.Sdk", None] //DODO verison?
3434
| d -> d
3535
let properties =
3636
directives |> List.choose (function Property(n, v) -> Some(n.ToLowerInvariant(), v) | _ -> None) |> Map
@@ -41,10 +41,11 @@ let createProjectFileLines directives entryPointSourceFullPath artifactsPath ass
4141
[
4242
"<Project>"
4343
" <PropertyGroup>"
44-
$""" <AssemblyName>{assemblyName}</AssemblyName>"""
44+
$""" <AssemblyName>{escape assemblyName}</AssemblyName>"""
4545
" <UseArtifactsOutput>true</UseArtifactsOutput>"
4646
" <IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>"
4747
$""" <ArtifactsPath>{escape artifactsPath}</ArtifactsPath>"""
48+
$""" <FileBasedProgram>true</FileBasedProgram>"""
4849
" </PropertyGroup>"
4950
yield! sdks |> List.map (sdkLine "Sdk.props")
5051
" <PropertyGroup>"

src/Runfs/Runfs.fs

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ open Runfs.Directives
88
open Runfs.ProjectFile
99
open Runfs.Dependencies
1010
open Runfs.Utilities
11+
open Runfs.Build
1112

1213
type RunfsError =
1314
| CaughtException of Exception
1415
| InvalidSourcePath of string
1516
| InvalidSourceDirectory of string
1617
| DirectiveError of ParseError list
17-
| RestoreError of stdout: string list * stderr: string list
18-
| BuildError of stdout: string list * stderr: string list
18+
| BuildError of MSBuildError
1919

2020
let ThisPackageName = "Runfs"
2121
let DependenciesHashFileName = "dependencies.hash"
@@ -58,11 +58,12 @@ let run (options, sourcePath, args) =
5858
let showTimings = Set.contains "time" options
5959
let verbose = Set.contains "verbose" options
6060
let noDependencyCheck = Set.contains "no-dependency-check" options
61-
let withOutput = Set.contains "with-output" options
6261
let inline guardAndTime name f = guardAndTime showTimings name f
6362

63+
initMSBuild()
64+
6465
result {
65-
let! fullSourcePath, fullSourceDir, artifactsDir, projectFilePath,
66+
let! fullSourcePath, fullSourceDir, artifactsDir, virtualProjectFilePath,
6667
savedProjectFilePath, dependenciesHashPath, sourceHashPath, dllPath =
6768
guardAndTime "creating paths" <| fun () -> result {
6869
do! File.Exists sourcePath |> Result.requireTrue (InvalidSourcePath sourcePath)
@@ -106,7 +107,7 @@ let run (options, sourcePath, args) =
106107
return computeDependenciesHash (string fullSourceDir) directives
107108
}
108109

109-
let! dependenciesChanged, sourceChanged, noDll = guardAndTime "computing build level" <| fun () ->
110+
let! dependenciesChanged, sourceChanged, noExecutable = guardAndTime "computing build level" <| fun () ->
110111
let dependenciesChanged =
111112
if noDependencyCheck then
112113
false
@@ -119,55 +120,37 @@ let run (options, sourcePath, args) =
119120
let noDll = not (File.Exists dllPath)
120121
Ok (dependenciesChanged, sourceChanged, noDll)
121122

122-
if dependenciesChanged || sourceChanged || noDll then
123+
if dependenciesChanged || noExecutable then
123124
do! guardAndTime "creating and writing project file" <| fun () ->
124125
let projectFileLines = createProjectFileLines directives fullSourcePath artifactsDir AssemblyName
125126
File.WriteAllLines(savedProjectFilePath, projectFileLines) |> Ok
127+
128+
if dependenciesChanged || sourceChanged || noExecutable then
129+
use! project = guardAndTime "creating msbuild project instance" <| fun () ->
130+
let projectFileText = File.ReadAllText savedProjectFilePath
131+
createProject verbose virtualProjectFilePath projectFileText |> Ok
132+
133+
if dependenciesChanged || noExecutable then
134+
do! guardAndTime "running msbuild restore" <| fun () -> result {
135+
File.Delete dependenciesHashPath
136+
do! build "restore" project |> Result.mapError BuildError
137+
}
126138

127-
if dependenciesChanged || noDll then
128-
do! guardAndTime "running dotnet restore" <| fun () ->
129-
File.Delete dependenciesHashPath
130-
if File.Exists projectFilePath then File.Delete projectFilePath
131-
File.Copy(savedProjectFilePath, projectFilePath)
132-
let args = [
133-
"restore"
134-
if not verbose then "-v:q"
135-
projectFilePath
136-
]
137-
let exitCode, stdoutLines, stderrLines =
138-
runCommandCollectOutput "dotnet" args fullSourceDir
139-
File.Delete projectFilePath
140-
if exitCode <> 0 then Error(RestoreError(stdoutLines, stderrLines)) else Ok()
141-
142-
if sourceChanged || dependenciesChanged || noDll then
143-
do! guardAndTime "running dotnet build" <| fun () ->
144-
if File.Exists projectFilePath then File.Delete projectFilePath
145-
File.Copy(savedProjectFilePath, projectFilePath)
146-
let args = [
147-
"build"
148-
"--no-restore"
149-
"-consoleLoggerParameters:NoSummary"
150-
if not verbose then "-v:q"
151-
projectFilePath
152-
]
153-
let exitCode, stdoutLines, stderrLines =
154-
runCommandCollectOutput "dotnet" args fullSourceDir
155-
File.Delete projectFilePath
156-
if exitCode <> 0 then Error(BuildError(stdoutLines, stderrLines)) else Ok()
157-
158-
if dependenciesChanged then
159-
do! guardAndTime "saving dependencies hash" <| fun () ->
160-
File.WriteAllText(dependenciesHashPath, dependenciesHash) |> Ok
161-
162-
if sourceChanged then
163-
do! guardAndTime "saving source hash" <| fun () ->
164-
File.WriteAllText(sourceHashPath, sourceHash) |> Ok
139+
do! guardAndTime "running dotnet build" <| fun () -> result {
140+
File.Delete sourceHash
141+
do! build "build" project |> Result.mapError BuildError
142+
}
143+
144+
if dependenciesChanged then
145+
do! guardAndTime "saving dependencies hash" <| fun () ->
146+
File.WriteAllText(dependenciesHashPath, dependenciesHash) |> Ok
147+
148+
if sourceChanged then
149+
do! guardAndTime "saving source hash" <| fun () ->
150+
File.WriteAllText(sourceHashPath, sourceHash) |> Ok
165151

166152
let! exitCode = guardAndTime "executing program" <| fun () ->
167-
if withOutput then
168-
runCommandCollectOutput "dotnet" (dllPath::args) "." |> Ok
169-
else
170-
runCommand "dotnet" (dllPath::args) "." |> Ok
153+
runCommand "dotnet" (dllPath::args) "." |> Ok
171154

172155
return exitCode
173156
}

0 commit comments

Comments
 (0)