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

Cannot create Fable plugin #3625

Open
OrfeasZ opened this issue Nov 30, 2023 · 12 comments
Open

Cannot create Fable plugin #3625

OrfeasZ opened this issue Nov 30, 2023 · 12 comments

Comments

@OrfeasZ
Copy link
Contributor

OrfeasZ commented Nov 30, 2023

I'm trying to create a Fable plugin but I'm having some trouble.

As a very basic starting point, I tried creating the following attribute:

namespace Test

open Fable

type TestPluginAttribute() =
    inherit MemberDeclarationPluginAttribute()
    override self.FableMinimumVersion = "4.5.0"
    override self.Transform(compiler, file, decl) = decl
    override self.TransformCall(compiler, memb, expr) = expr

However, when trying to compile my project with Fable, I get the following error:

error FABLE: Cannot reference entity from .dll reference, Fable packages must include F# sources: Fable.MemberDeclarationPluginAttribute

My project file looks like this:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <RootNamespace>Test</RootNamespace>
        <GenerateDocumentationFile>true</GenerateDocumentationFile>
        <AssemblyName>Test</AssemblyName>
    </PropertyGroup>
    <ItemGroup>
        <Compile Include="Test.fs"/>
    </ItemGroup>
    <ItemGroup>
        <PackageReference Include="Fable.AST" Version="4.3.0"/>
    </ItemGroup>
</Project>

This is included from another F# project, and compiled using the following command (fable version is 4.5.0):

dotnet fable -o out

What am I doing wrong?

@MangelMaxime
Copy link
Member

Hello,

When consuming a plugin in local development you need to tell Fable to exclude it from the compilation.

For example, dotnet fable --exclude Fable.CompilerPlugins - o out

Does doing so solve your problem?

@OrfeasZ
Copy link
Contributor Author

OrfeasZ commented Nov 30, 2023

I did try that earlier (ie. --exclude Test), but then my other project fails to compile where I use the attribute:

error FSHARP: The namespace or module 'Test' is not defined. (code 39)
error FSHARP: The type 'TestPlugin' is not defined. (code 39)

@nojaf
Copy link
Member

nojaf commented Nov 30, 2023

You may need to have Plugin in your assembly name for the filtering to kick in.
I did some experiment with a plugin a while ago and I had --exclude "Nojaf.Fable.React.Plugin"

My full command was:
dotnet fable ./Playground.fsproj -c Debug -e .js -o ./out --noReflection --exclude "Nojaf.Fable.React.Plugin"

@nojaf
Copy link
Member

nojaf commented Nov 30, 2023

Nah, that might not be it. Do you have

// Tell Fable to scan for plugins in this assembly
[<assembly : ScanForPlugins>]
do ()

in your plugin assembly as well?

@OrfeasZ
Copy link
Contributor Author

OrfeasZ commented Nov 30, 2023

That unfortunately doesn't seem to change anything. Also tried using the .Plugin suffix but that didn't work either.

I've uploaded a minimal repro here: https://github.com/OrfeasZ/fable-plugin-repro

The command I'm using is:

dotnet fable ./FablePluginTest/FablePluginTest.fsproj -o out --exclude FablePluginTest.Plugin

Maybe it has to do with how the plugin project is referenced from the main project?

@MangelMaxime
Copy link
Member

So I was able to debug the problem.

If you look at the console output, you should see an error about Fable not being able to compile the Project because of

System.ArgumentException: The index is outside the legal range.
count = 1, array.Length = 0 (Parameter 'count')

This is coming from:

let excludeProjRef
(opts: CrackerOptions)
(dllRefs: IDictionary<string, string>)
(projRef: string)
=
let projName = Path.GetFileNameWithoutExtension(projRef)
let isExcluded =
opts.Exclude
|> List.exists (fun e ->
String.Equals(
e,
Path.GetFileNameWithoutExtension(projRef),
StringComparison.OrdinalIgnoreCase
)
)
if isExcluded then
try
opts.BuildDll(dllRefs[projName])
with e ->
Log.always ("Couldn't build " + projName + ": " + e.Message)
None
else
let _removed = dllRefs.Remove(projName)
// if not removed then
// Log.always("Couldn't remove project reference " + projName + " from dll references")
Path.normalizeFullPath projRef |> Some

But the actual error exception is occurring from here:

member _.BuildDll(normalizedDllPath: string) =
if not (builtDlls.Contains(normalizedDllPath)) then
let projDir =
normalizedDllPath.Split('/')
|> Array.rev
|> Array.skipWhile (fun part -> part <> "bin")
|> Array.skip 1
|> Array.rev
|> String.concat "/"
Process.runSync
projDir
"dotnet"
[
"build"
"-c"
cliArgs.Configuration
]
|> ignore
builtDlls.Add(normalizedDllPath) |> ignore

The problem is that for some reason, FablePluginTest.Plugin reference is coming from the obj folder instead of the bin folder.

So when Fable tries to |> Array.skipWhile (fun part -> part <> "bin") it never find bin and fails on the next line trying to |> Array.skip 1 an empty array.

Changing bin to obj solve this problem, but then when Fable tries to compile the code I have an error occurring from the plugin application:

Project and references (1 source files) parsed in 2026ms

Loaded FablePluginTest.Plugin.TestPluginAttribute from FablePluginTest.Plugin/obj/Release/net8.0/ref/FablePluginTest.Plugin.dll
Started Fable compilation...
Fable compilation finished in 739ms  

./FablePluginTest/Test.fs(1,1): error EXCEPTION: Exception has been thrown by the target of an invocation.
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)
   at System.Activator.CreateInstance(Type type, Boolean nonPublic, Boolean wrapExceptions)
   at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture)
   at System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)
   at Fable.CompilerExt.Compiler-ApplyPlugin@249.Invoke(Input input, Attribute att) in /Users/mmangel/Workspaces/Github/fable-compiler/Fable/src/Fable.Transforms/Global/Compiler.fs:line 253
   at Microsoft.FSharp.Collections.SeqModule.Fold[T,TState](FSharpFunc`2 folder, TState state, IEnumerable`1 source) in /home/dev/Projects/fsharp/src/FSharp.Core/seq.fs:line 912
   at Fable.CompilerExt.Compiler.ApplyMemberDeclarationPlugin(Compiler com, File file, MemberDecl decl) in /Users/mmangel/Workspaces/Github/fable-compiler/Fable/src/Fable.Transforms/Global/Compiler.fs:line 286
   at Fable.Transforms.FableTransforms.transformDeclaration(FSharpList`1 transformations, Compiler com, File file, Declaration decl) in /Users/mmangel/Workspaces/Github/fable-compiler/Fable/src/Fable.Transforms/FableTransforms.fs:line 996
   at Fable.Transforms.FableTransforms.newDecls@1087.Invoke(Declaration decl)
   at Microsoft.FSharp.Primitives.Basics.List.mapToFreshConsTail[a,b](FSharpList`1 cons, FSharpFunc`2 f, FSharpList`1 x) in /home/dev/Projects/fsharp/src/FSharp.Core/local.fs:line 236
   at Microsoft.FSharp.Primitives.Basics.List.map[T,TResult](FSharpFunc`2 mapping, FSharpList`1 x) in /home/dev/Projects/fsharp/src/FSharp.Core/local.fs:line 246
   at Microsoft.FSharp.Collections.ListModule.Map[T,TResult](FSharpFunc`2 mapping, FSharpList`1 list) in /home/dev/Projects/fsharp/src/FSharp.Core/list.fs:line 97
   at Fable.Transforms.FableTransforms.transformFile(Compiler com, File file) in /Users/mmangel/Workspaces/Github/fable-compiler/Fable/src/Fable.Transforms/FableTransforms.fs:line 1085
   at Fable.Cli.Pipeline.Js.compileFile@241.Invoke(Unit unitVar) in /Users/mmangel/Workspaces/Github/fable-compiler/Fable/src/Fable.Cli/Pipeline.fs:line 242
   at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvoke[T,TResult](AsyncActivation`1 ctxt, TResult result1, FSharpFunc`2 part2) in /home/dev/Projects/fsharp/src/FSharp.Core/async.fs:line 510
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in /home/dev/Projects/fsharp/src/FSharp.Core/async.fs:line 112
Compilation failed

I am not sure if this is because of a bug in the plugin implementation or in Fable code itself.

I believe the problem is related the .NET SDK used when running Fable. Indeed, forcing the repository to use .NET 6, then everything is working correctly.

@nojaf @ncave any ideas?

Luckily, the problem seems to only occurs when working with local plugin, so projects in production that use Feliz (for example) and .NET still works.


@OrfeasZ If you want to work on your plugin, you can force you repository to use .NET 6 via a global.json.

{
    "sdk": {
        "version": "6.0.100",
        "rollForward": "latestFeature"
    }
}

@OrfeasZ
Copy link
Contributor Author

OrfeasZ commented Nov 30, 2023

@MangelMaxime good catch, thanks a lot! Didn't even notice this error... 😅

Changing the plugin project to target .NET 6 does indeed fix this issue!

@OrfeasZ
Copy link
Contributor Author

OrfeasZ commented Nov 30, 2023

I can also confirm that ScanForPlugins is needed for the plugin to be invoked by Fable.

@nojaf
Copy link
Member

nojaf commented Dec 1, 2023

Only <TargetFramework> needs to be net6.0.
You can still use a higher SDK.
I'm guessing it needs to be net6.0, because the Fable CLI tool is on that version:

<TargetFramework>net6.0</TargetFramework>

@nojaf
Copy link
Member

nojaf commented Dec 1, 2023

@MangelMaxime where in https://github.com/fable-compiler/fable-compiler.github.io/tree/dev/docs/docs would be a good place to write some docs on what we learned in this thread?

@MangelMaxime
Copy link
Member

Currently, we have "Author a Fable library" in the "Your Fable project" section.

CleanShot 2023-12-01 at 09 30 20@2x

So we could add a page "Author a Fable plugin" / "Create a Fable plugin" below that page.

The other solution would be to create a section dedicated to Fable plugins, but creating a section for a single page seems a little wasteful to me.

My feeling currently is that "Your Fable project" name is not the correct name for that section, as "Author a Fable plugin" kind of feel out of place already in that section.

@OrfeasZ
Copy link
Contributor Author

OrfeasZ commented Dec 1, 2023

Only <TargetFramework> needs to be net6.0. You can still use a higher SDK. I'm guessing it needs to be net6.0, because the Fable CLI tool is on that version:

That's exactly what I did!

In regards to docs, I'm happy to contribute some parts after I've played with the plugin capabilities a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants