Fable
Fable copied to clipboard
Compile Error when referencing another project with `EntryPoint`
Description
When an application references another project with an EntryPoint (~main method), compilation fails with:
[...]/A/Program.fs(11,5): (11,9) error FSHARP: A function labeled with the 'EntryPointAttribute' attribute must be the last declaration in the last file in the compilation sequence. (code 433)
Repro code
(create directory for testing: mkdir tmp && cd tmp)
Create first project (in folder A)
dotnet new console -lang f# --output ./A
Create second project (in folder B)
dotnet new console -lang f# --output ./B
In B: Add reference to A
dotnet add ./B reference ./A
(
Because of #2365: Add module to top of Program.fs in both projects
@("module A") + (Get-Content .\A\Program.fs) | Out-File .\A\Program.fs
@("module B") + (Get-Content .\B\Program.fs) | Out-File .\B\Program.fs
)
Building:
dotnet build ./Adotnet build ./Bdotnet fable ./A
-> all succeed
dotnet fable ./B
Fable: F# to JS compiler 3.1.2
Thanks to the contributor! @halfabench
B> cmd /C dotnet restore B.fsproj
Determining projects to restore...
All projects are up-to-date for restore.
Parsing B\B.fsproj...
Initializing F# compiler...
Compiling B\B.fsproj...
F# compilation finished in 1252ms
[...]/tmp/A/Program.fs(11,5): (11,9) error FSHARP: A function labeled with the 'EntryPointAttribute' attribute must be the last declaration in the last file in the compilation sequence. (code 433)
Compilation failed
Expected and actual results
Expected: should compile -- just like the .NET version.
Actual: compilation error (see output above)
Both Program.fs files contain a main method with EntryPointAttribute. I guess Fable directly includes files from the other project -- and has now an EntryPoint in another place than the last file.
Probably doesn't happen very often in practice (but emerges in ts2fable), and quite easy to handle: extract main method of A into separate project, or define a symbol and put the EntryPointAttribute inside #if directive.
Related information
- Fable version:
dotnet fable --version: 3.1.2 - Dotnet version:
dotnet --version: 5.0.102
Yes, Fable collects all sources and builds a single project with them. I'm looking into avoiding this in the future but right now it's necessary to resolve inlined functions. Didn't know this worked in .NET, do you have an example when this can happens with ts2fable. Didn't know ts2fable used the EntryPoint attribute either.
hmpf...didn't mean to close the issue...just clicked wrong....
Happened while toying to update ts2fable to fable 3.
https://github.com/fable-compiler/ts2fable/blob/master/src/ts2fable.fs -> contains main method with EntryPoint ~= command line utility
https://github.com/fable-compiler/ts2fable/tree/master/web-app: References /src/tf2fable project. Is itself a project to run with Elmish (https://github.com/fable-compiler/ts2fable/blob/master/web-app/App.fs) -> second EntryPoint (though not explicitly specified)
Didn't know ts2fable used the EntryPoint attribute either.
Perhaps the CLI portion of ts2fable can be split out, so the library can be reused.
Ah, ok. So this in ts2fable itself, I thought it was in the code generated by ts2fable 😅 Probably the best solution is what @ncave proposes. I wonder how it was working with Fable before 🤔
Actual important reason to reference a project with EntryPoint: Testing
(happens in ts2fable repo too -- but wasn't at that step in converting to fable 3. And somehow didn't think of the general use case of testing when writing this issue....)
Usual structure: Some Main Project, and a Test Project that tests the things in the Main Project.
But if that main project contains an EntryPoint - bad luck. And extracting just the main method into an extra project is kind of strange: Because it's not testable it can only contain the main method which just calls the "real" main method in the other project. That's a lot of overhead...
I'm currently using a define and a compiler directive:
// other code
#if PROJECTNAME_STANDALONE
[<EntryPoint>]
let main (argv: string[]): int =
...
#endif
Requires to pass the symbol to the compiler when compiling the CLI project. But can still be referenced by other projects without any change.
Not perfect either, but at least not an extra project...
Yes, you're right, having an extra project only for the main function is cumbersome. Your solution with define constants is good. For node apps we can also use the trick to check if a file is being called directly as in this example:
[<Emit("""if (require.main === module) {
const code = $0(process.argv.slice(2));
process.exit(code);
}""")>]
let runMain(f: string[] -> int): unit = jsNative
// [<EntryPointAttribute>]
let main argv =
printfn "Running..."
0
runMain main