Open roboz0r opened 2 years ago
@roboz0r, not 100% sure, you may need to provide compilertool
option with path that contains the Nuget extension.
It would be something similar than what is done in this test: https://github.com/fsprojects/Paket/blob/eac93a466a2ed15c5b8bae3ad00e41d1dabc68f2/integrationtests/Paket.IntegrationTests/FsiExtension.fs#L36
@smoothdeveloper No luck sadly. I added --compilertool:C:\...\net6.0-windows
which was the path to the directory containing FSharp.DependencyManager.Nuget.dll
, FSharp.Compiler.Service.dll
, FSharp.Core
etc. I also tried various combinations /compilertool
instead of --
, --compilertool:C:\...\net6.0-windows\FSharp.DependencyManager.Nuget.dll
.
This might be a complete red herring but I noticed when I trigger compilation the AssemblyLoadContext
goes looking for FSharp.Core, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
. Is this correct and it will happily accept later FSharp.Core dlls?
For the assembly load, maybe you need something akin to https://github.com/fsprojects/Paket/blob/eac93a466a2ed15c5b8bae3ad00e41d1dabc68f2/integrationtests/Paket.IntegrationTests/FsiExtension.fs#L21-L28
That worked! Or at least compilation was successful. Now it fails when I try to call the function with:
TargetInvocationException: Could not load file or assembly 'FSharp.Data, Version=4.2.8.0, Culture=neutral, PublicKeyToken=49286adf818aa259'. The system cannot find the file specified.
at Main.getSomeData()
It exists at "C:\Users\Robert\.nuget\packages\fsharp.data\4.2.8\lib\netstandard2.0\FSharp.Data.dll"
. Is there a simple way to either have the compiler copy the needed dlls into the compile dir (I didn't see anything on the Compiler Options listed)? Or a pre-built utility function that knows how to search the local nuget dir? I'm sure I can get something working from here but it will be less error-prone if something is readily available.
Thanks for your help with this.
I've come up with a solution that seems to work. Pasting below so that others can make use of it:
open System
open System.IO
open System.Reflection
open System.Runtime.Loader
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Text
type MyContext(sharedTypes:Assembly, scriptPath:string, nugetDlls: string seq) =
inherit AssemblyLoadContext("MyContext", true)
let dllDir = Path.Combine(Path.GetDirectoryName(scriptPath), "bin")
let dllName = Guid.NewGuid().ToString()
let dllFullName = Path.Combine(dllDir, $"%s{dllName}.dll")
let mutable scriptAssembly = Unchecked.defaultof<Assembly>
let mutable resolver : AssemblyDependencyResolver option = None
//https://learn.microsoft.com/en-us/dotnet/api/system.runtime.loader.assemblydependencyresolver.-ctor?view=net-6.0#system-runtime-loader-assemblydependencyresolver-ctor(system-string)
// componentAssemblyPath can be a relative or an absolute path. The assembly must have an accompanying deps.json file in the same directory that describes the assembly's dependencies. This file is output during the build process.
// Compiler produces ".\bin\Projects\28244--2f36d849-4688-4031-8ecf-713772ead307\bin\Debug\net6.0\win-x64\Project.deps.json"
// Rename to <dll-name>.deps.json and copy to dll folder
let copyDepsJson () =
let dir = DirectoryInfo(dllDir)
let toFind = "Project.deps.json"
let files = dir.GetFiles(toFind, SearchOption.AllDirectories)
match files.Length with
| 0 -> logger.Warn $"Could not locate %s{toFind}"
| 1 ->
let dest = Path.ChangeExtension(dllFullName, ".deps.json")
files[0].CopyTo(dest, true) |> ignore
// Cannot create resolver until deps.json is in place
resolver <- new AssemblyDependencyResolver(dllFullName) |> Some
logger.Debug $"Created resolver for %s{dest}"
| i ->
logger.Debug $"Located {i} {toFind}. Selecting newest."
files
|> Array.sortInPlaceBy (fun file -> file.CreationTimeUtc)
let dest = Path.ChangeExtension(dllFullName, ".deps.json")
(Array.last files).CopyTo(dest, true) |> ignore
resolver <- new AssemblyDependencyResolver(dllFullName) |> Some
logger.Debug $"Created resolver for %s{dest}"
let copyNugetDlls () =
for dll in nugetDlls do
logger.Debug($"%s{dllName}: Copying %s{dll}")
let file = FileInfo(dll)
let destFile = Path.Combine(dllDir, Path.GetFileName(dll))
if File.Exists destFile then ()
else file.CopyTo(destFile) |> ignore
override this.Load(assemblyName) =
// Must use same Assembly instances where possible (instead of loading Assembly again from disk) otherwise types won't match and aren't compatible.
let fullName = assemblyName.FullName
logger.Debug($"%s{dllName}: Resolving {fullName}")
if fullName = sharedTypes.FullName then
logger.Debug($"%s{dllName}: Supplied {sharedTypes.FullName}")
sharedTypes
else
AssemblyLoadContext.GetLoadContext(sharedTypes).Assemblies
|> Seq.tryFind (fun x -> x.FullName = fullName)
|> function
| Some a ->
logger.Debug($"%s{dllName}: SharedTypes Assemblies Supplied {a.Location}")
a
| None ->
match resolver with
| Some resolver ->
resolver.ResolveAssemblyToPath(assemblyName)
|> function
| null ->
logger.Debug($"%s{dllName}: Failed to resolve {fullName}")
null
| path ->
logger.Debug($"%s{dllName}: AssemblyDependencyResolver Supplied {path}")
this.LoadFromAssemblyPath path
| None ->
logger.Debug($"%s{dllName}: Failed to resolve {fullName}")
null
member __.Assembly = scriptAssembly
member __.DllDir = dllDir
member __.DllFullName = dllFullName
member this.LoadCompiledDll() =
copyNugetDlls ()
copyDepsJson ()
scriptAssembly <- this.LoadFromAssemblyPath(dllFullName)
module Checker =
let private checker = FSharpChecker.Create()
// https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/compiler-options
let private compilerArgs (scriptPath:string) (dllPath) =
[|
"-a"; scriptPath
$"-o:%s{dllPath}"
"--targetprofile:netcore"
"--target:library"
"--debug:full"
// Needed to ensure Projects dirs appear with dllPath
$"--compilertool:%s{Assembly.GetExecutingAssembly().DirectoryName}"
|]
// Based on https://queil.net/2021/06/embedding-fsharp-compiler-nuget-references/
let resolveNugets (scriptPath) =
async {
let source = File.ReadAllText scriptPath |> SourceText.ofString
let! projOptions, errors = checker.GetProjectOptionsFromScript(scriptPath, source)
match errors with
| [] ->
let! projResults = checker.ParseAndCheckProject(projOptions)
return
match projResults.HasCriticalErrors with
| false ->
projResults.DependencyFiles
|> Array.filter(fun path -> path.EndsWith(".dll"))
|> Set.ofArray
|> Ok
| _ -> Error (List.ofArray projResults.Diagnostics)
| _ -> return Error (errors)
}
let tryCompileCtx sharedTypes scriptPath logger =
async {
let! nugetDlls = resolveNugets scriptPath
match nugetDlls with
| Ok nugetDlls ->
let loadCtx = MyContext(sharedTypes, scriptPath, logger, nugetDlls)
let compilerArgs = compilerArgs scriptPath loadCtx.DllFullName
let! errors, retCode = checker.Compile(compilerArgs)
let maybeAssembly =
match retCode with
| 0 ->
loadCtx.LoadCompiledDll()
Some loadCtx
| _ -> None
return
match maybeAssembly with
| Some a ->
Ok (a, List.ofArray errors)
| None ->
Error (retCode, scriptPath, List.ofArray errors, List.ofArray compilerArgs)
| Error errors -> return Error (-1, scriptPath, errors, [])
}
// Ensure the hosting process' AssemblyLoadContext can also access FSharp.Core
do
AssemblyLoadContext.Default.add_Resolving (fun alc name ->
logger.Debug $"Default AssemblyLoadContext searching for: {name.FullName}"
if name.FullName.Contains("FSharp.Core") then
logger.Debug $"Providing local FSharp.Core"
typedefof<Map<_,_>>.Assembly
else null
)
I am building a tool to enable F# scripting within a native windows application. I've run into an issue with the ability to use
#r nuget
directives.I have two different ways I am calling the compiler against the script files:
Producing a dynamic assembly
Producing a dll (to enable debugging)
Both of these versions of the code pass basic tests to compile and run the following script in the test environment (running with Visual Studio):
However, when attempting to compile the same script calling
tryCompileCtx
in the native hosted environment I get the following errors (usingtryCompile
works as expected):Using FSharp.Core 6.0.3, FSharp.Compiler.Service 41.0.3
Using FSharp.Core 6.0.6, FSharp.Compiler.Service 41.0.6
Based on the phrasing of the errors and the fact that it is different between 41.0.3 and 41.0.6 I'm fairly confident the error originates from an exception thrown in DependencyProvider.fs. However I have no idea how to track it down further than that or what to do about it.
Compiling a simple script like
let hello name = $"Hello, %s{name}"
works as expected in all cases.Is this a bug or is there some additional configuration I need to provide so that
#r nuget
directives will work as expected?If it's relevant, I'm running:
dotnet --version 6.0.401
.