Skip to content

Commit 6c42e61

Browse files
committed
Merge pull request #864 from fsharp/cache
Add caching / fix line break tests
2 parents 1a9b971 + ce20956 commit 6c42e61

9 files changed

+377
-26
lines changed

paket.dependencies

+1
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ nuget xunit.extensions
3636
nuget Newtonsoft.Json
3737
nuget Microsoft.AspNet.Razor 2.0.30506
3838
nuget Microsoft.AspNet.WebPages 2.0.30506
39+
nuget HashLib

paket.lock

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ NUGET
1212
FsCheck.Xunit (1.0.4)
1313
FsCheck (>= 1.0.4)
1414
xunit (>= 1.9.2)
15-
FSharp.Compiler.Service (0.0.89)
15+
FSharp.Compiler.Service (1.3.1.0)
1616
FSharp.Core (3.1.2.1)
1717
FSharp.Formatting (2.9.3)
1818
FSharp.Compiler.Service (>= 0.0.87)
1919
FSharpVSPowerTools.Core (1.8.0)
2020
FSharp.Formatting.CommandTool (2.9.3)
2121
FSharpVSPowerTools.Core (1.8.0)
2222
FSharp.Compiler.Service (>= 0.0.87)
23+
HashLib (2.0.1)
2324
jQuery (2.1.3)
2425
Knockout (0.0.1)
2526
AspNetMvc (>= 4.0.0.0)
@@ -79,4 +80,4 @@ NUGET
7980
xunit (1.9.2)
8081
xunit.extensions (1.9.2)
8182
xunit (1.9.2)
82-
xunit.runners (1.9.2)
83+
xunit.runners (1.9.2)

src/app/FAKE/Cli.fs

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type FakeArg =
1515
| [<AltCommandLine("-b")>] [<Rest>] Boot of string
1616
| [<AltCommandLine("-br")>] Break
1717
| [<AltCommandLine("-st")>] Single_Target
18+
| [<AltCommandLine("-nc")>] NoCache
1819
interface IArgParserTemplate with
1920
member x.Usage =
2021
match x with
@@ -27,6 +28,7 @@ type FakeArg =
2728
| Boot _ -> "Boostrapp your FAKE script."
2829
| Break -> "Pauses FAKE with a Debugger.Break() near the start"
2930
| Single_Target -> "Runs only the specified target and not the dependencies."
31+
| NoCache -> "Disables caching of compiled script"
3032

3133
/// Return the parsed FAKE args or the parse exception.
3234
let parsedArgsOrEx args =

src/app/FAKE/Program.fs

+3-2
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ try
116116

117117
//TODO if printDetails then printEnvironment cmdArgs args
118118

119-
if not (runBuildScriptWithFsiArgsAt printDetails fsiArgs envVars) then Environment.ExitCode <- 1
119+
let useCache = not (fakeArgs.Contains <@ Cli.NoCache @>)
120+
if not (runBuildScriptWithFsiArgsAt printDetails fsiArgs envVars useCache true) then Environment.ExitCode <- 1
120121
else if printDetails then log "Ready."
121122

122123
()
@@ -135,7 +136,7 @@ try
135136
let printDetails = containsParam "details" cmdArgs
136137
if printDetails then
137138
printEnvironment cmdArgs args
138-
if not (runBuildScript printDetails buildScriptArg fsiArgs args) then Environment.ExitCode <- 1
139+
if not (runBuildScript printDetails buildScriptArg fsiArgs args true true) then Environment.ExitCode <- 1
139140
else if printDetails then log "Ready."
140141
| Some handler ->
141142
handler.Interact()

src/app/FakeLib/FSIHelper.fs

+158-21
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,49 @@ open System.Threading
1010

1111
let private FSIPath = @".\tools\FSharp\;.\lib\FSharp\;[ProgramFilesX86]\Microsoft SDKs\F#\4.0\Framework\v4.0;[ProgramFilesX86]\Microsoft SDKs\F#\3.1\Framework\v4.0;[ProgramFilesX86]\Microsoft SDKs\F#\3.0\Framework\v4.0;[ProgramFiles]\Microsoft F#\v4.0\;[ProgramFilesX86]\Microsoft F#\v4.0\;[ProgramFiles]\FSharp-2.0.0.0\bin\;[ProgramFilesX86]\FSharp-2.0.0.0\bin\;[ProgramFiles]\FSharp-1.9.9.9\bin\;[ProgramFilesX86]\FSharp-1.9.9.9\bin\"
1212

13+
let createDirectiveRegex id =
14+
Text.RegularExpressions.Regex(
15+
"^\s*#" + id + "\s*(@\"|\"\"\"|\")(?<path>.+?)(\"\"\"|\")",
16+
System.Text.RegularExpressions.RegexOptions.Compiled |||
17+
System.Text.RegularExpressions.RegexOptions.Multiline)
18+
let loadRegex = createDirectiveRegex "load"
19+
let rAssemblyRegex = createDirectiveRegex "r"
20+
let searchPathRegex = createDirectiveRegex "I"
21+
22+
let private extractDirectives (regex : System.Text.RegularExpressions.Regex) scriptContents =
23+
regex.Matches(scriptContents)
24+
|> Seq.cast<Text.RegularExpressions.Match>
25+
|> Seq.map(fun m ->
26+
(m.Groups.Item("path").Value)
27+
)
28+
let rec getAllScripts scriptPath : seq<string * string> =
29+
let scriptPath =
30+
if Path.IsPathRooted scriptPath then
31+
scriptPath
32+
else
33+
Path.Combine(Directory.GetCurrentDirectory(), scriptPath)
34+
let scriptContents = File.ReadAllText(scriptPath)
35+
let loadedContents =
36+
extractDirectives loadRegex scriptContents
37+
|> Seq.collect(fun path ->
38+
let path =
39+
if Path.IsPathRooted path then
40+
path
41+
else
42+
Path.Combine(Path.GetDirectoryName(scriptPath), path)
43+
getAllScripts path
44+
)
45+
Seq.concat [List.toSeq [scriptPath, scriptContents]; loadedContents]
46+
47+
let getAllScriptContents (pathsAndContents : seq<string * string>) =
48+
pathsAndContents |> Seq.map(snd)
49+
let getIncludedAssembly scriptContents = extractDirectives rAssemblyRegex scriptContents
50+
let getSearchPaths scriptContents = extractDirectives searchPathRegex scriptContents
51+
52+
let getScriptHash pathsAndContents =
53+
let fullContents = getAllScriptContents pathsAndContents |> String.concat("\n")
54+
let hasher = HashLib.HashFactory.Checksum.CreateCRC32a()
55+
hasher.ComputeString(fullContents).ToString()
1356
/// The path to the F# Interactive tool.
1457
let fsiPath =
1558
let ev = environVar "FSI"
@@ -87,9 +130,14 @@ let executeFSIWithScriptArgsAndReturnMessages script (scriptArgs: string[]) =
87130

88131
open Microsoft.FSharp.Compiler.Interactive.Shell
89132

133+
type private AssemblySource =
134+
| GAC
135+
| Disk
136+
137+
let hashRegex = Text.RegularExpressions.Regex("(?<script>.+)_(?<hash>[a-zA-Z0-9]+\.dll$)", System.Text.RegularExpressions.RegexOptions.Compiled)
90138
/// Run the given FAKE script with fsi.exe at the given working directory. Provides full access to Fsi options and args. Redirect output and error messages.
91-
let internal runFAKEScriptWithFsiArgsAndRedirectMessages printDetails (FsiArgs(fsiOptions, script, scriptArgs)) args onErrMsg onOutMsg =
92-
if printDetails then traceFAKE "Running Buildscript: %s" script
139+
let internal runFAKEScriptWithFsiArgsAndRedirectMessages printDetails (FsiArgs(fsiOptions, scriptPath, scriptArgs)) args onErrMsg onOutMsg useCache cleanCache =
140+
if printDetails then traceFAKE "Running Buildscript: %s" scriptPath
93141

94142
// Add arguments to the Environment
95143
for (k,v) in args do
@@ -113,31 +161,118 @@ let internal runFAKEScriptWithFsiArgsAndRedirectMessages printDetails (FsiArgs(f
113161
then onMsg s
114162
handleMessagesFrom sbOut onOutMsg
115163
handleMessagesFrom sbErr onErrMsg
164+
let handleException (ex : Exception) =
165+
onErrMsg (ex.ToString())
116166

117167
use outStream = new StringWriter(sbOut)
118168
use errStream = new StringWriter(sbErr)
119169
use stdin = new StreamReader(Stream.Null)
120170

121-
try
122-
let session = FsiEvaluationSession.Create(fsiConfig, commonOptions, stdin, outStream, errStream)
171+
let allScriptContents = getAllScripts scriptPath
172+
let scriptHash = lazy (getScriptHash allScriptContents)
173+
//TODO this is only calculating the hash for the input file, not anything #load-ed
174+
175+
let scriptFileName = lazy(Path.GetFileName(scriptPath))
176+
let hashPath = lazy("./.fake/" + scriptFileName.Value + "_" + scriptHash.Value)
177+
let assemblyPath = lazy(hashPath.Value + ".dll")
178+
let assemblyRefPath = lazy(hashPath.Value + "_references.txt")
179+
let cacheValid = lazy (
180+
System.IO.File.Exists(assemblyPath.Value) &&
181+
System.IO.File.Exists(assemblyRefPath.Value))
182+
183+
let getScriptAndHash fileName =
184+
let matched = hashRegex.Match(fileName)
185+
matched.Groups.Item("script").Value, matched.Groups.Item("hash").Value
186+
187+
if useCache && cacheValid.Value then
188+
189+
trace ("Using cache")
190+
let noExtension = Path.GetFileNameWithoutExtension(scriptFileName.Value)
191+
let fullName =
192+
sprintf "<StartupCode$FSI_0001>.$FSI_0001_%s%s$%s"
193+
(noExtension.Substring(0, 1).ToUpper())
194+
(noExtension.Substring(1))
195+
(Path.GetExtension(scriptFileName.Value).Substring(1))
123196

197+
for loc in File.ReadAllLines(assemblyRefPath.Value) do
198+
Reflection.Assembly.LoadFrom(loc) |> ignore
199+
200+
let assembly = Reflection.Assembly.LoadFrom(assemblyPath.Value)
201+
202+
let mainModule = assembly.GetType(fullName)
203+
124204
try
125-
session.EvalScript script
126-
// TODO: Reactivate when FCS don't show output any more
127-
// handleMessages()
205+
let _result =
206+
mainModule.InvokeMember(
207+
"main@",
208+
System.Reflection.BindingFlags.InvokeMethod |||
209+
System.Reflection.BindingFlags.Public |||
210+
System.Reflection.BindingFlags.Static, null, null, [||])
128211
true
129212
with
130-
| _ ->
131-
handleMessages()
213+
| ex ->
214+
handleException ex
132215
false
133-
with
134-
| exn ->
135-
traceError "FsiEvaluationSession could not be created."
136-
traceError <| sbErr.ToString()
137-
raise exn
216+
else
217+
if useCache then
218+
let cacheDir = DirectoryInfo("./.fake")
219+
if cacheDir.Exists then
220+
let oldFiles =
221+
cacheDir.GetFiles()
222+
|> Seq.filter(fun file ->
223+
let oldScriptName, _ = getScriptAndHash(file.Name)
224+
oldScriptName = scriptFileName.Value
225+
)
226+
if (oldFiles |> Seq.length) > 0 then
227+
if cleanCache then
228+
for file in oldFiles do
229+
file.Delete()
230+
trace "Cache is invalid, recompiling"
231+
else
232+
trace "Cache doesnt exist"
233+
else
234+
trace "Cache doesnt exist"
235+
try
236+
let session = FsiEvaluationSession.Create(fsiConfig, commonOptions, stdin, outStream, errStream)
237+
try
238+
session.EvalScript scriptPath
239+
240+
try
241+
if useCache && not cacheValid.Value then
242+
let assemBuilder = session.DynamicAssembly :?> System.Reflection.Emit.AssemblyBuilder
243+
assemBuilder.Save("FSI-ASSEMBLY.dll")
244+
Directory.CreateDirectory("./.fake") |> ignore
245+
File.Move("./FSI-ASSEMBLY.dll", assemblyPath.Value)
246+
247+
if File.Exists("./FSI-ASSEMBLY.pdb") then
248+
File.Delete("./FSI-ASSEMBLY.pdb")
249+
250+
let refedAssemblies =
251+
System.AppDomain.CurrentDomain.GetAssemblies()
252+
|> Seq.filter(fun assem -> not assem.IsDynamic)
253+
|> Seq.map(fun assem -> assem.Location)
254+
255+
File.WriteAllLines(assemblyRefPath.Value, refedAssemblies) |> ignore
256+
trace (System.Environment.NewLine + "Saved cache")
257+
with
258+
| ex ->
259+
handleException ex
260+
reraise()
261+
// TODO: Reactivate when FCS don't show output any more
262+
// handleMessages()
263+
true
264+
with
265+
| _ex ->
266+
handleMessages()
267+
false
268+
with
269+
| exn ->
270+
traceError "FsiEvaluationSession could not be created."
271+
traceError <| sbErr.ToString()
272+
raise exn
138273

139274
/// Run the given buildscript with fsi.exe and allows for extra arguments to the script. Returns output.
140-
let executeBuildScriptWithArgsAndReturnMessages script (scriptArgs: string[]) =
275+
let executeBuildScriptWithArgsAndReturnMessages script (scriptArgs: string[]) useCache cleanCache =
141276
let messages = ref []
142277
let appendMessage isError msg =
143278
messages := { IsError = isError
@@ -146,19 +281,21 @@ let executeBuildScriptWithArgsAndReturnMessages script (scriptArgs: string[]) =
146281
let result =
147282
runFAKEScriptWithFsiArgsAndRedirectMessages
148283
true (FsiArgs([], script, scriptArgs |> List.ofArray)) []
149-
(appendMessage true) (appendMessage false)
284+
(appendMessage true) (appendMessage false) useCache cleanCache
150285
(result, !messages)
151286

152287
/// Run the given buildscript with fsi.exe at the given working directory. Provides full access to Fsi options and args.
153-
let runBuildScriptWithFsiArgsAt printDetails (FsiArgs(fsiOptions, script, scriptArgs)) args =
288+
let runBuildScriptWithFsiArgsAt printDetails (FsiArgs(fsiOptions, script, scriptArgs)) args useCache cleanCache =
154289
runFAKEScriptWithFsiArgsAndRedirectMessages
155290
printDetails (FsiArgs(fsiOptions, script, scriptArgs)) args
156291
traceError (fun s-> traceFAKE "%s" s)
292+
useCache
293+
cleanCache
157294

158295
/// Run the given buildscript with fsi.exe at the given working directory.
159-
let runBuildScriptAt printDetails script extraFsiArgs args =
160-
runBuildScriptWithFsiArgsAt printDetails (FsiArgs(extraFsiArgs, script, [])) args
296+
let runBuildScriptAt printDetails script extraFsiArgs args useCache cleanCache =
297+
runBuildScriptWithFsiArgsAt printDetails (FsiArgs(extraFsiArgs, script, [])) args useCache cleanCache
161298

162299
/// Run the given buildscript with fsi.exe
163-
let runBuildScript printDetails script extraFsiArgs args =
164-
runBuildScriptAt printDetails script extraFsiArgs args
300+
let runBuildScript printDetails script extraFsiArgs args useCache cleanCache =
301+
runBuildScriptAt printDetails script extraFsiArgs args useCache cleanCache

src/app/FakeLib/FakeLib.fsproj

+11
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,17 @@
271271
</ItemGroup>
272272
</When>
273273
</Choose>
274+
<Choose>
275+
<When Condition="($(TargetFrameworkIdentifier) == '.NETFramework' And ($(TargetFrameworkVersion) == 'v4.0' Or $(TargetFrameworkVersion) == 'v4.5' Or $(TargetFrameworkVersion) == 'v4.5.1' Or $(TargetFrameworkVersion) == 'v4.5.2' Or $(TargetFrameworkVersion) == 'v4.5.3' Or $(TargetFrameworkVersion) == 'v4.6')) Or ($(TargetFrameworkIdentifier) == 'MonoAndroid') Or ($(TargetFrameworkIdentifier) == 'MonoTouch')">
276+
<ItemGroup>
277+
<Reference Include="HashLib">
278+
<HintPath>..\..\..\packages\HashLib\lib\net40\HashLib.dll</HintPath>
279+
<Private>True</Private>
280+
<Paket>True</Paket>
281+
</Reference>
282+
</ItemGroup>
283+
</When>
284+
</Choose>
274285
<Choose>
275286
<When Condition="($(TargetFrameworkIdentifier) == '.NETFramework' And ($(TargetFrameworkVersion) == 'v4.0' Or $(TargetFrameworkVersion) == 'v4.5' Or $(TargetFrameworkVersion) == 'v4.5.1' Or $(TargetFrameworkVersion) == 'v4.5.2' Or $(TargetFrameworkVersion) == 'v4.5.3' Or $(TargetFrameworkVersion) == 'v4.6')) Or ($(TargetFrameworkIdentifier) == 'MonoAndroid') Or ($(TargetFrameworkIdentifier) == 'MonoTouch')">
276287
<ItemGroup>

src/app/FakeLib/paket.references

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ FSharp.Compiler.Service
33
Mono.Web.Xdt
44
Mono.Cecil
55
Nuget.Core
6-
Newtonsoft.Json
6+
Newtonsoft.Json
7+
HashLib

0 commit comments

Comments
 (0)