Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type provider design time component loading suitable for .NET Core tooling #3864

Merged
merged 7 commits into from Dec 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/FSharpSource.Profiles.targets
Expand Up @@ -10,6 +10,9 @@
<DefineConstants>$(DefineConstants);FX_LCIDFROMCODEPAGE</DefineConstants>
</PropertyGroup>

<!-- Note: The TargetDotnetProfile value 'coreclr' currently covers all of netstandard1.6, netstandard2.0 and netcoreapp2.0 in a confused way -->
<!-- These should be distinguished in the future -->

<PropertyGroup Condition="'$(TargetDotnetProfile)'=='coreclr'">
<DefineConstants>$(DefineConstants);FX_PORTABLE_OR_NETSTANDARD</DefineConstants>
<DefineConstants>$(DefineConstants);NETSTANDARD1_6</DefineConstants>
Expand Down
17 changes: 9 additions & 8 deletions src/fsharp/CompileOps.fs
Expand Up @@ -4274,14 +4274,16 @@ type TcImports(tcConfigP:TcConfigProvider, initialResolutions:TcAssemblyResoluti

// Find assembly level TypeProviderAssemblyAttributes. These will point to the assemblies that
// have class which implement ITypeProvider and which have TypeProviderAttribute on them.
let providerAssemblies =
let designTimeAssemblyNames =
runtimeAssemblyAttributes
|> List.choose (TryDecodeTypeProviderAssemblyAttr (defaultArg ilGlobalsOpt EcmaMscorlibILGlobals))
// If no design-time assembly is specified, use the runtime assembly
|> List.map (function null -> Path.GetFileNameWithoutExtension fileNameOfRuntimeAssembly | s -> s)
|> Set.ofList
|> List.map (function null -> fileNameOfRuntimeAssembly | s -> s)
// For each simple name of a design-time assembly, we take the first matching one in the order they are
// specified in the attributes
|> List.distinctBy (fun s -> try Path.GetFileNameWithoutExtension(s) with _ -> s)

if providerAssemblies.Count > 0 then
if designTimeAssemblyNames.Length > 0 then

// Find the SystemRuntimeAssemblyVersion value to report in the TypeProviderConfig.
let primaryAssemblyVersion =
Expand Down Expand Up @@ -4309,10 +4311,9 @@ type TcImports(tcConfigP:TcConfigProvider, initialResolutions:TcAssemblyResoluti
fun arg -> systemRuntimeContainsTypeRef.Value arg

let providers =
[ for assemblyName in providerAssemblies do
yield ExtensionTyping.GetTypeProvidersOfAssembly(fileNameOfRuntimeAssembly, ilScopeRefOfRuntimeAssembly, assemblyName, typeProviderEnvironment,
tcConfig.isInvalidationSupported, tcConfig.isInteractive, systemRuntimeContainsType, primaryAssemblyVersion, m) ]
let providers = providers |> List.concat
[ for designTimeAssemblyName in designTimeAssemblyNames do
yield! ExtensionTyping.GetTypeProvidersOfAssembly(fileNameOfRuntimeAssembly, ilScopeRefOfRuntimeAssembly, designTimeAssemblyName, typeProviderEnvironment,
tcConfig.isInvalidationSupported, tcConfig.isInteractive, systemRuntimeContainsType, primaryAssemblyVersion, m) ]

// Note, type providers are disposable objects. The TcImports owns the provider objects - when/if it is disposed, the providers are disposed.
// We ignore all exceptions from provider disposal.
Expand Down
104 changes: 83 additions & 21 deletions src/fsharp/ExtensionTyping.fs
Expand Up @@ -36,6 +36,37 @@ module internal ExtensionTyping =
temporaryFolder : string }


// Specify the tooling-compatible fragments of a path such as:
// typeproviders/fsharp41/net461/MyProvider.DesignTime.dll
// See https://github.com/Microsoft/visualfsharp/issues/3736

// Represents the FF#-compiler <-> type provider protocol.
// When the API or protocol updates, add a new version moniker to the front of the list here.
let toolingCompatibleTypeProviderProtocolMonikers() =
[ "fsharp41" ]

// Detect the host tooling context
let toolingCompatibleVersions() =
if typeof<obj>.Assembly.GetName().Name = "mscorlib" then
[ "net461"; "net452"; "net451"; "net45"; "netstandard2.0"]
elif typeof<obj>.Assembly.GetName().Name = "System.Private.CoreLib" then
[ "netcoreapp2.0"; "netstandard2.0"]
else
System.Diagnostics.Debug.Assert(false, "Couldn't determine runtime tooling context, assuming it supports at least .NET Standard 2.0")
[ "netstandard2.0"]

// When significant new processor types appear add a new moniker here. Note that use of this qualifier will be very rare
// and we don't expect different design-time assemblies will be needed for different architectures very often. Some
// exceptions may be design-time components for type providers for systems such as Python or R.
let toolingCompatibleArch() = if sizeof<nativeint> = 8 then "x64" else "x86"
let toolingCompatiblePaths() =
[ for protocol in toolingCompatibleTypeProviderProtocolMonikers() do
for netRuntime in toolingCompatibleVersions() do
let dir = Path.Combine("typeproviders", protocol, netRuntime)
yield Path.Combine(dir, toolingCompatibleArch())
yield dir
]

/// Load a the design-time part of a type-provider into the host process, and look for types
/// marked with the TypeProviderAttribute attribute.
let GetTypeProviderImplementationTypes (runTimeAssemblyFileName, designTimeAssemblyNameString, m:range) =
Expand All @@ -46,32 +77,58 @@ module internal ExtensionTyping =

// Find and load the designer assembly for the type provider component.
//
// If the assembly name ends with .dll, or is just a simple name, we look in the directory next to runtime assembly.
// Else we only look in the GAC.
let designTimeAssemblyOpt =
let loadFromDir fileName =
// We look in the directories stepping up from the location of the runtime assembly.

let loadFromLocation designTimeAssemblyPath =
try
Some (FileSystem.AssemblyLoadFrom designTimeAssemblyPath)
with e ->
raiseError e

let rec searchParentDirChain dir designTimeAssemblyName =
seq {
for subdir in toolingCompatiblePaths() do
let designTimeAssemblyPath = Path.Combine (dir, subdir, designTimeAssemblyName)
if FileSystem.SafeExists designTimeAssemblyPath then
yield loadFromLocation designTimeAssemblyPath
match Path.GetDirectoryName(dir) with
| s when s = "" || s = null || Path.GetFileName(dir) = "packages" || s = dir -> ()
| parentDir -> yield! searchParentDirChain parentDir designTimeAssemblyName
}

let loadFromParentDirRelativeToRuntimeAssemblyLocation designTimeAssemblyName =
let runTimeAssemblyPath = Path.GetDirectoryName runTimeAssemblyFileName
searchParentDirChain runTimeAssemblyPath designTimeAssemblyName
|> Seq.tryHead
|> function
| Some res -> res
| None ->
// The search failed, just load from the first location and report an error
let runTimeAssemblyPath = Path.GetDirectoryName runTimeAssemblyFileName
let designTimeAssemblyPath = Path.Combine (runTimeAssemblyPath, fileName)
try
Some (FileSystem.AssemblyLoadFrom designTimeAssemblyPath)
with e ->
raiseError e
let loadFromGac() =
try
let asmName = System.Reflection.AssemblyName designTimeAssemblyNameString
Some (FileSystem.AssemblyLoad (asmName))
with e ->
raiseError e
loadFromLocation (Path.Combine (runTimeAssemblyPath, designTimeAssemblyName))

let designTimeAssemblyOpt =

if designTimeAssemblyNameString.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) then
loadFromDir designTimeAssemblyNameString
loadFromParentDirRelativeToRuntimeAssemblyLocation designTimeAssemblyNameString
else
let name = System.Reflection.AssemblyName designTimeAssemblyNameString
// Cover the case where the ".dll" extension has been left off and no version etc. has been used in the assembly
// string specification. The Name=FullName comparison is particularly strange, and was there to support
// design-time DLLs specified using "x.DesignTIme, Version= ..." long assembly names and GAC loads.
// These kind of design-time assembly specifications are no longer used to our knowledge so that comparison is basically legacy
// and will always succeed.
let name = System.Reflection.AssemblyName (Path.GetFileNameWithoutExtension designTimeAssemblyNameString)
if name.Name.Equals(name.FullName, StringComparison.OrdinalIgnoreCase) then
let fileName = designTimeAssemblyNameString+".dll"
loadFromDir fileName
let designTimeAssemblyName = designTimeAssemblyNameString+".dll"
loadFromParentDirRelativeToRuntimeAssemblyLocation designTimeAssemblyName
else
loadFromGac()
// Load from the GAC using Assembly.Load. This is legacy since type provider design-time components are
// never in the GAC these days and "x.DesignTIme, Version= ..." specifications are never used.
try
let asmName = System.Reflection.AssemblyName designTimeAssemblyNameString
Some (FileSystem.AssemblyLoad (asmName))
with e ->
raiseError e

// If we've find a design-time assembly, look for the public types with TypeProviderAttribute
match designTimeAssemblyOpt with
Expand Down Expand Up @@ -152,12 +209,17 @@ module internal ExtensionTyping =
try
let designTimeAssemblyName =
try
Some (System.Reflection.AssemblyName designTimeAssemblyNameString)
if designTimeAssemblyNameString.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) then
Some (System.Reflection.AssemblyName (Path.GetFileNameWithoutExtension designTimeAssemblyNameString))
else
Some (System.Reflection.AssemblyName designTimeAssemblyNameString)
with :? ArgumentException ->
errorR(Error(FSComp.SR.etInvalidTypeProviderAssemblyName(runTimeAssemblyFileName, designTimeAssemblyNameString), m))
None

[ match designTimeAssemblyName, resolutionEnvironment.outputFile with
// Check if the attribute is pointing to the file being compiled, in which case ignore it
// This checks seems like legacy but is included for compat.
| Some designTimeAssemblyName, Some path when String.Compare(designTimeAssemblyName.Name, Path.GetFileNameWithoutExtension path, StringComparison.OrdinalIgnoreCase) = 0 ->
()
| Some _, _ ->
Expand Down
3 changes: 3 additions & 0 deletions src/fsharp/ExtensionTyping.fsi
Expand Up @@ -24,6 +24,9 @@ module internal ExtensionTyping =
/// Raised when an type provider has thrown an exception.
exception ProvidedTypeResolutionNoRange of exn

/// Get the list of relative paths searched for type provider design-time components
val toolingCompatiblePaths: unit -> string list

/// Carries information about the type provider resolution environment.
type ResolutionEnvironment =
{
Expand Down
37 changes: 29 additions & 8 deletions src/fsharp/FSharp.Compiler.Unittests/ProductVersion.fs
Expand Up @@ -82,11 +82,11 @@ module ProductVersionTest =
"0.0.0.0", (0us,0us,0us,0us)
"3213.57843.32382.59493", (3213us,57843us,32382us,59493us)
(sprintf "%d.%d.%d.%d" max max max max), (max,max,max,max) ]
|> List.map (fun (s,e) -> TestCaseData(s, e))

[<TestCaseSource("validValues")>]
let ``should use values if valid major.minor.revision.build version format`` (v, expected) =
v |> productVersionToILVersionInfo |> Assert.areEqual expected
[<Test>]
let ``should use values if valid major.minor.revision.build version format`` () =
for (v, expected) in validValues() do
v |> productVersionToILVersionInfo |> Assert.areEqual expected

let invalidValues () =
[ "1.2.3.4", (1us,2us,3us,4us)
Expand All @@ -100,8 +100,29 @@ module ProductVersionTest =
"", (0us,0us,0us,0us)
"70000.80000.90000.100000", (0us,0us,0us,0us)
(sprintf "%d.70000.80000.90000" System.UInt16.MaxValue), (System.UInt16.MaxValue,0us,0us,0us) ]
|> List.map (fun (s,e) -> TestCaseData(s, e))

[<TestCaseSource("invalidValues")>]
let ``should zero starting from first invalid version part`` (v, expected) =
v |> productVersionToILVersionInfo |> Assert.areEqual expected
[<Test>]
let ``should zero starting from first invalid version part`` () =
for (v, expected) in invalidValues() do
v |> productVersionToILVersionInfo |> Assert.areEqual expected

module TypeProviderDesignTimeComponentLoading =


[<Test>]
let ``check tooling paths for type provider design time component loading`` () =
let arch = if sizeof<nativeint> = 8 then "x64" else "x86"
let expected =
[ @"typeproviders\fsharp41\net461\" + arch
@"typeproviders\fsharp41\net461"
@"typeproviders\fsharp41\net452\" + arch
@"typeproviders\fsharp41\net452"
@"typeproviders\fsharp41\net451\" + arch
@"typeproviders\fsharp41\net451"
@"typeproviders\fsharp41\net45\" + arch
@"typeproviders\fsharp41\net45"
@"typeproviders\fsharp41\netstandard2.0\" + arch
@"typeproviders\fsharp41\netstandard2.0"
]
let actual = Microsoft.FSharp.Compiler.ExtensionTyping.toolingCompatiblePaths()
Assert.areEqual expected actual
83 changes: 82 additions & 1 deletion tests/fsharp/TypeProviderTests.fs
Expand Up @@ -23,9 +23,11 @@ open SingleTest
// Use these lines if you want to test CoreCLR
let FSC_BASIC = FSC_CORECLR
let FSI_BASIC = FSI_CORECLR
let FSIANYCPU_BASIC = FSI_CORECLR
#else
let FSC_BASIC = FSC_OPT_PLUS_DEBUG
let FSI_BASIC = FSI_FILE
let FSIANYCPU_BASIC = FSIANYCPU_FILE
#endif

[<Test>]
Expand Down Expand Up @@ -248,14 +250,93 @@ let ``negative type provider tests`` (name:string) =

[<Test>]
let splitAssembly () =

let cfg = testConfig "typeProviders/splitAssembly"

let clean() =
rm cfg "providerDesigner.dll"
rmdir cfg "typeproviders"
rmdir cfg (".." ++ "typeproviders")

clean()

fsc cfg "--out:provider.dll -a" ["provider.fs"]

fsc cfg "--out:providerDesigner.dll -a" ["providerDesigner.fsx"]

SingleTest.singleTestBuildAndRunAux cfg FSC_BASIC


SingleTest.singleTestBuildAndRunAux cfg FSI_BASIC

SingleTest.singleTestBuildAndRunAux cfg FSIANYCPU_BASIC

// Do the same thing with different load locations for the type provider design-time component

clean()

// check a few load locations
let someLoadPaths =
[ "typeproviders" ++ "fsharp41" ++ "net461" ++ "x86"
"typeproviders" ++ "fsharp41" ++ "net461"
"typeproviders" ++ "fsharp41" ++ "net45"
// include up one directory
".." ++ "typeproviders" ++ "fsharp41" ++ "net45"
"typeproviders" ++ "fsharp41" ++ "netstandard2.0" ]

let someLoadPaths64 =
[ "typeproviders" ++ "fsharp41" ++ "net461" ++ "x64"
"typeproviders" ++ "fsharp41" ++ "net461" ]

let someNegativeLoadPaths64 =
[ "typeproviders" ++ "fsharp41" ++ "net461" ++ "x86" ]


for dir in someLoadPaths do

clean()

// put providerDesigner.dll into a different place
mkdir cfg dir
fsc cfg "--out:%s/providerDesigner.dll -a" dir ["providerDesigner.fsx"]

SingleTest.singleTestBuildAndRunAux cfg FSC_BASIC

for dir in someLoadPaths do

clean()

// put providerDesigner.dll into a different place
mkdir cfg dir
fsc cfg "--out:%s/providerDesigner.dll -a" dir ["providerDesigner.fsx"]

SingleTest.singleTestBuildAndRunAux cfg FSI_BASIC

for dir in someLoadPaths64 do

clean()

// put providerDesigner.dll into a different place
mkdir cfg dir
fsc cfg "--out:%s/providerDesigner.dll -a" dir ["providerDesigner.fsx"]

SingleTest.singleTestBuildAndRunAux cfg FSIANYCPU_BASIC

for dir in someNegativeLoadPaths64 do

clean()

// put providerDesigner.dll into a different place
mkdir cfg dir
fsc cfg "--out:%s/providerDesigner.dll -a" dir ["providerDesigner.fsx"]

// We expect a failure here - an error correctly gets printed on the console
try
SingleTest.singleTestBuildAndRunAux cfg FSIANYCPU_BASIC |> ignore
failwith "expected an AssertionException"
with :? NUnit.Framework.AssertionException -> ()

clean()

[<Test>]
let wedgeAssembly () =
let cfg = testConfig "typeProviders/wedgeAssembly"
Expand Down
9 changes: 9 additions & 0 deletions tests/fsharp/single-test.fs
Expand Up @@ -12,6 +12,7 @@ type Permutation =
| FSI_CORECLR
#if !FSHARP_SUITE_DRIVES_CORECLR_TESTS
| FSI_FILE
| FSIANYCPU_FILE
| FSI_STDIN
| GENERATED_SIGNATURE
| FSC_OPT_MINUS_DEBUG
Expand Down Expand Up @@ -94,6 +95,14 @@ let singleTestBuildAndRunCore cfg (copyFiles:string) p =

testOkFile.CheckExists()

| FSIANYCPU_FILE ->
use cleanup = (cleanUpFSharpCore cfg)
use testOkFile = new FileGuard (getfullpath cfg "test.ok")

fsiAnyCpu cfg "%s" cfg.fsi_flags sources

testOkFile.CheckExists()

| FSI_STDIN ->
use cleanup = (cleanUpFSharpCore cfg)
use testOkFile = new FileGuard (getfullpath cfg "test.ok")
Expand Down