Fable icon indicating copy to clipboard operation
Fable copied to clipboard

Compile Error when referencing another project with `EntryPoint`

Open Booksbaum opened this issue 4 years ago • 6 comments
trafficstars

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 ./A dotnet build ./B dotnet 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

Booksbaum avatar Jan 26 '21 20:01 Booksbaum

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.

alfonsogarciacaro avatar Jan 27 '21 06:01 alfonsogarciacaro

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)

Booksbaum avatar Jan 27 '21 07:01 Booksbaum

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.

ncave avatar Jan 27 '21 18:01 ncave

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 🤔

alfonsogarciacaro avatar Jan 27 '21 23:01 alfonsogarciacaro

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...

Booksbaum avatar Jan 28 '21 19:01 Booksbaum

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

alfonsogarciacaro avatar Jan 29 '21 04:01 alfonsogarciacaro